refactor(emqx_auth_mnesia): use tag e4.2.2

Removed emqx_auth_clientid and emqx_auth_username
because the new version emqx_auth_mnesia has all the features included
This commit is contained in:
Zaiming Shi 2020-12-02 17:49:48 +01:00
parent 5f0f91bb15
commit e236196fa6
35 changed files with 1067 additions and 2021 deletions

View File

@ -1,117 +0,0 @@
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.

View File

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

View File

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

View File

@ -1,26 +0,0 @@
%%-*- 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}.

View File

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

View File

@ -1,14 +0,0 @@
{application, emqx_auth_clientid,
[{description, "EMQ X Authentication with ClientId/Password"},
{vsn, "5.0.0"}, % strict semver, bump manually!
{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"}
]}
]}.

View File

@ -1,170 +0,0 @@
%%--------------------------------------------------------------------
%% 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>>.

View File

@ -1,127 +0,0 @@
%%--------------------------------------------------------------------
%% 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.

View File

@ -1,53 +0,0 @@
%%--------------------------------------------------------------------
%% 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}, []} }.

View File

@ -1,191 +0,0 @@
%% 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.

View File

@ -0,0 +1,30 @@
## Password hash.
##
## Value: plain | md5 | sha | sha256 | sha512
auth.mnesia.password_hash = sha256
##--------------------------------------------------------------------
## ClientId Authentication
##--------------------------------------------------------------------
## Examples
##auth.client.1.clientid = id
##auth.client.1.password = passwd
##auth.client.2.clientid = dev:devid
##auth.client.2.password = passwd2
##auth.client.3.clientid = app:appid
##auth.client.3.password = passwd3
##auth.client.4.clientid = client~!@#$%^&*()_+
##auth.client.4.password = passwd~!@#$%^&*()_+
##--------------------------------------------------------------------
## Username Authentication
##--------------------------------------------------------------------
## Examples:
##auth.user.1.username = admin
##auth.user.1.password = public
##auth.user.2.username = feng@emqtt.io
##auth.user.2.password = public
##auth.user.3.username = name~!@#$%^&*()_+
##auth.user.3.password = pwsswd~!@#$%^&*()_+

View File

@ -1,17 +1,20 @@
-define(APP, emqx_auth_mnesia). -define(APP, emqx_auth_mnesia).
-type(login():: {clientid, binary()}
| {username, binary()}).
-record(emqx_user, { -record(emqx_user, {
login, login :: login(),
password, password :: binary(),
is_superuser created_at :: integer()
}). }).
-record(emqx_acl, { -record(emqx_acl, {
login, filter:: {login() | all, emqx_topic:topic()},
topic, action :: pub | sub | pubsub,
action, access :: allow | deny,
allow created_at :: integer()
}). }).
-record(auth_metrics, { -record(auth_metrics, {
success = 'client.auth.success', success = 'client.auth.success',

View File

@ -1,34 +1,42 @@
%%-*- mode: erlang -*- %%-*- mode: erlang -*-
%% emqx_auth_mnesia config mapping %% 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", [ {mapping, "auth.mnesia.password_hash", "emqx_auth_mnesia.password_hash", [
{default, sha256}, {default, sha256},
{datatype, {enum, [plain, md5, sha, sha256]}} {datatype, {enum, [plain, md5, sha, sha256, sha512]}}
]}. ]}.
{mapping, "auth.mnesia.$id.login", "emqx_auth_mnesia.userlist", [ {mapping, "auth.client.$id.clientid", "emqx_auth_mnesia.clientid_list", [
{datatype, string} {datatype, string}
]}. ]}.
{mapping, "auth.mnesia.$id.password", "emqx_auth_mnesia.userlist", [ {mapping, "auth.client.$id.password", "emqx_auth_mnesia.clientid_list", [
{datatype, string} {datatype, string}
]}. ]}.
{mapping, "auth.mnesia.$id.is_superuser", "emqx_auth_mnesia.userlist", [ {translation, "emqx_auth_mnesia.clientid_list", fun(Conf) ->
{default, false}, ClientList = cuttlefish_variable:filter_by_prefix("auth.client", Conf),
{datatype, {enum, [false, true]}}
]}.
{translation, "emqx_auth_mnesia.userlist", fun(Conf) ->
Userlist = cuttlefish_variable:filter_by_prefix("auth.mnesia", Conf),
lists:foldl( lists:foldl(
fun({["auth", "mnesia", Id, "login"], Username}, AccIn) -> fun({["auth", "client", Id, "clientid"], ClientId}, AccIn) ->
[{Username, cuttlefish:conf_get("auth.mnesia." ++ Id ++ ".password", Conf), cuttlefish:conf_get("auth.mnesia." ++ Id ++ ".is_superuser", Conf)} | AccIn]; [{ClientId, cuttlefish:conf_get("auth.client." ++ Id ++ ".password", Conf)} | AccIn];
(_, AccIn) ->
AccIn
end, [], ClientList)
end}.
{mapping, "auth.user.$id.username", "emqx_auth_mnesia.username_list", [
{datatype, string}
]}.
{mapping, "auth.user.$id.password", "emqx_auth_mnesia.username_list", [
{datatype, string}
]}.
{translation, "emqx_auth_mnesia.username_list", fun(Conf) ->
Userlist = cuttlefish_variable:filter_by_prefix("auth.user", Conf),
lists:foldl(
fun({["auth", "user", Id, "username"], Username}, AccIn) ->
[{Username, cuttlefish:conf_get("auth.user." ++ Id ++ ".password", Conf)} | AccIn];
(_, AccIn) -> (_, AccIn) ->
AccIn AccIn
end, [], Userlist) end, [], Userlist)

View File

@ -1 +1,29 @@
{deps, []}. {minimum_otp_vsn, "21"}.
{deps,
[{emqx_passwd, {git, "https://github.com/emqx/emqx-passwd.git", {tag, "v1.1.1"}}},
{minirest, {git, "https://github.com/emqx/minirest.git", {tag, "0.3.0"}}}
]}.
{profiles,
[{test,
[{deps,
[{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.2.2"}}}
]}
]}
]}.
{erl_opts, [warn_unused_vars,
warn_shadow_vars,
warn_unused_import,
warn_obsolete_guard,
debug_info,
{parse_transform}]}.
{xref_checks, [undefined_function_calls, undefined_functions,
locals_not_used, deprecated_function_calls,
warnings_as_errors, deprecated_functions]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
{cover_export_enabled, true}.

View File

@ -18,6 +18,10 @@
-include("emqx_auth_mnesia.hrl"). -include("emqx_auth_mnesia.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-define(TABLE, emqx_acl).
%% ACL Callbacks %% ACL Callbacks
-export([ init/0 -export([ init/0
, register_metrics/0 , register_metrics/0
@ -27,22 +31,38 @@
init() -> init() ->
ok = ekka_mnesia:create_table(emqx_acl, [ ok = ekka_mnesia:create_table(emqx_acl, [
{type, bag},
{disc_copies, [node()]}, {disc_copies, [node()]},
{attributes, record_info(fields, emqx_acl)}, {attributes, record_info(fields, emqx_acl)},
{storage_properties, [{ets, [{read_concurrency, true}]}]}]), {storage_properties, [{ets, [{read_concurrency, true}]}]}]),
ok = ekka_mnesia:copy_table(emqx_user, disc_copies). ok = ekka_mnesia:copy_table(emqx_acl, disc_copies).
-spec(register_metrics() -> ok). -spec(register_metrics() -> ok).
register_metrics() -> register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
check_acl(ClientInfo, PubSub, Topic, NoMatchAction, #{key_as := As}) -> check_acl(ClientInfo = #{ clientid := Clientid }, PubSub, Topic, _NoMatchAction, _Params) ->
Login = maps:get(As, ClientInfo), Username = maps:get(username, ClientInfo, undefined),
case do_check_acl(Login, PubSub, Topic, NoMatchAction) of
ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok; Acls = case Username of
{stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow}; undefined ->
{stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny} emqx_acl_mnesia_cli:lookup_acl({clientid, Clientid}) ++
emqx_acl_mnesia_cli:lookup_acl(all);
_ ->
emqx_acl_mnesia_cli:lookup_acl({clientid, Clientid}) ++
emqx_acl_mnesia_cli:lookup_acl({username, Username}) ++
emqx_acl_mnesia_cli:lookup_acl(all)
end,
case match(ClientInfo, PubSub, Topic, Acls) of
allow ->
emqx_metrics:inc(?ACL_METRICS(allow)),
{stop, allow};
deny ->
emqx_metrics:inc(?ACL_METRICS(deny)),
{stop, deny};
_ ->
emqx_metrics:inc(?ACL_METRICS(ignore)),
ok
end. end.
description() -> "Acl with Mnesia". description() -> "Acl with Mnesia".
@ -51,33 +71,33 @@ description() -> "Acl with Mnesia".
%% Internal functions %% Internal functions
%%------------------------------------------------------------------- %%-------------------------------------------------------------------
do_check_acl(Login, PubSub, Topic, _NoMatchAction) -> match(_ClientInfo, _PubSub, _Topic, []) ->
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; nomatch;
match(PubSub, Topic, [ #emqx_acl{topic = ACLTopic, action = Action, allow = Allow} | UserAcl]) -> match(ClientInfo, PubSub, Topic, [ {_, ACLTopic, Action, Access, _} | Acls]) ->
case match_actions(PubSub, Action) andalso match_topic(Topic, ACLTopic) of case match_actions(PubSub, Action) andalso match_topic(ClientInfo, Topic, ACLTopic) of
true -> case Allow of true -> Access;
true -> allow; false -> match(ClientInfo, PubSub, Topic, Acls)
_ -> deny
end;
false -> match(PubSub, Topic, UserAcl)
end. end.
match_topic(Topic, ACLTopic) when is_binary(Topic) -> match_topic(ClientInfo, Topic, ACLTopic) when is_binary(Topic) ->
emqx_topic:match(Topic, ACLTopic). emqx_topic:match(Topic, feed_var(ClientInfo, ACLTopic)).
match_actions(_, <<"pubsub">>) -> true; match_actions(_, pubsub) -> true;
match_actions(subscribe, <<"sub">>) -> true; match_actions(subscribe, sub) -> true;
match_actions(publish, <<"pub">>) -> true; match_actions(publish, pub) -> true;
match_actions(_, _) -> false. match_actions(_, _) -> false.
feed_var(ClientInfo, Pattern) ->
feed_var(ClientInfo, emqx_topic:words(Pattern), []).
feed_var(_ClientInfo, [], Acc) ->
emqx_topic:join(lists:reverse(Acc));
feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) ->
feed_var(ClientInfo, Words, [<<"%c">>|Acc]);
feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) ->
feed_var(ClientInfo, Words, [ClientId |Acc]);
feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) ->
feed_var(ClientInfo, Words, [<<"%u">>|Acc]);
feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) ->
feed_var(ClientInfo, Words, [Username|Acc]);
feed_var(ClientInfo, [W|Words], Acc) ->
feed_var(ClientInfo, Words, [W|Acc]).

View File

@ -1,4 +1,4 @@
%%-------------------------------------------------------------------- %c%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%% %%
%% Licensed under the Apache License, Version 2.0 (the "License"); %% Licensed under the Apache License, Version 2.0 (the "License");
@ -18,94 +18,174 @@
-include("emqx_auth_mnesia.hrl"). -include("emqx_auth_mnesia.hrl").
-import(proplists, [get_value/2]). -include_lib("stdlib/include/ms_transform.hrl").
-import(proplists, [ get_value/2
, get_value/3
]).
-import(minirest, [return/1]). -import(minirest, [return/1]).
-rest_api(#{name => list_emqx_acl, -rest_api(#{name => list_clientid,
method => 'GET', method => 'GET',
path => "/mqtt_acl", path => "/acl/clientid",
func => list, func => list_clientid,
descr => "List available mnesia in the cluster" descr => "List available mnesia in the cluster"
}). }).
-rest_api(#{name => lookup_emqx_acl, -rest_api(#{name => list_username,
method => 'GET', method => 'GET',
path => "/mqtt_acl/:bin:login", path => "/acl/username",
func => list_username,
descr => "List available mnesia in the cluster"
}).
-rest_api(#{name => list_all,
method => 'GET',
path => "/acl/$all",
func => list_all,
descr => "List available mnesia in the cluster"
}).
-rest_api(#{name => lookup_clientid,
method => 'GET',
path => "/acl/clientid/:bin:clientid",
func => lookup, func => lookup,
descr => "Lookup mnesia in the cluster" descr => "Lookup mnesia in the cluster"
}). }).
-rest_api(#{name => add_emqx_acl, -rest_api(#{name => lookup_username,
method => 'GET',
path => "/acl/username/:bin:username",
func => lookup,
descr => "Lookup mnesia in the cluster"
}).
-rest_api(#{name => add,
method => 'POST', method => 'POST',
path => "/mqtt_acl", path => "/acl",
func => add, func => add,
descr => "Add mnesia in the cluster" descr => "Add mnesia in the cluster"
}). }).
-rest_api(#{name => delete_emqx_acl, -rest_api(#{name => delete_clientid,
method => 'DELETE', method => 'DELETE',
path => "/mqtt_acl/:bin:login/:bin:topic", path => "/acl/clientid/:bin:clientid/topic/:bin:topic",
func => delete, func => delete,
descr => "Delete mnesia in the cluster" descr => "Delete mnesia in the cluster"
}). }).
-export([ list/2 -rest_api(#{name => delete_username,
method => 'DELETE',
path => "/acl/username/:bin:username/topic/:bin:topic",
func => delete,
descr => "Delete mnesia in the cluster"
}).
-rest_api(#{name => delete_all,
method => 'DELETE',
path => "/acl/$all/topic/:bin:topic",
func => delete,
descr => "Delete mnesia in the cluster"
}).
-export([ list_clientid/2
, list_username/2
, list_all/2
, lookup/2 , lookup/2
, add/2 , add/2
, delete/2 , delete/2
]). ]).
list(_Bindings, Params) -> list_clientid(_Bindings, Params) ->
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, Params, fun format/1)}). MatchSpec = ets:fun2ms(
fun({emqx_acl, {{clientid, Clientid}, Topic}, Action, Access, CreatedAt}) -> {{clientid,Clientid}, Topic, Action,Access, CreatedAt} end),
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}).
lookup(#{login := Login}, _Params) -> list_username(_Bindings, Params) ->
return({ok, format(emqx_auth_mnesia_cli:lookup_acl(urldecode(Login)))}). MatchSpec = ets:fun2ms(
fun({emqx_acl, {{username, Username}, Topic}, Action, Access, CreatedAt}) -> {{username, Username}, Topic, Action,Access, CreatedAt} end),
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}).
list_all(_Bindings, Params) ->
MatchSpec = ets:fun2ms(
fun({emqx_acl, {all, Topic}, Action, Access, CreatedAt}) -> {all, Topic, Action,Access, CreatedAt}end
),
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}).
lookup(#{clientid := Clientid}, _Params) ->
return({ok, format(emqx_acl_mnesia_cli:lookup_acl({clientid, urldecode(Clientid)}))});
lookup(#{username := Username}, _Params) ->
return({ok, format(emqx_acl_mnesia_cli:lookup_acl({username, urldecode(Username)}))}).
add(_Bindings, Params) -> add(_Bindings, Params) ->
[ P | _] = Params, [ P | _] = Params,
case is_list(P) of case is_list(P) of
true -> return(add_acl(Params, [])); true -> return(do_add(Params, []));
false -> return(add_acl([Params], [])) false ->
Re = do_add(Params),
case Re of
#{result := ok} -> return({ok, Re});
#{result := <<"ok">>} -> return({ok, Re});
_ -> return({error, Re})
end
end. end.
add_acl([ Params | ParamsN ], ReList ) -> do_add([ Params | ParamsN ], ReList) ->
Login = urldecode(get_value(<<"login">>, Params)), do_add(ParamsN, [do_add(Params) | ReList]);
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) -> do_add([], ReList) ->
{ok, ReList}. {ok, ReList}.
delete(#{login := Login, topic := Topic}, _) -> do_add(Params) ->
return(emqx_auth_mnesia_cli:remove_acl(urldecode(Login), urldecode(Topic))). Clientid = get_value(<<"clientid">>, Params, undefined),
Username = get_value(<<"username">>, Params, undefined),
Login = case {Clientid, Username} of
{undefined, undefined} -> all;
{_, undefined} -> {clientid, urldecode(Clientid)};
{undefined, _} -> {username, urldecode(Username)}
end,
Topic = urldecode(get_value(<<"topic">>, Params)),
Action = urldecode(get_value(<<"action">>, Params)),
Access = urldecode(get_value(<<"access">>, Params)),
Re = case validate([login, topic, action, access], [Login, Topic, Action, Access]) of
ok ->
emqx_acl_mnesia_cli:add_acl(Login, Topic, erlang:binary_to_atom(Action, utf8), erlang:binary_to_atom(Access, utf8));
Err -> Err
end,
maps:merge(#{topic => Topic,
action => Action,
access => Access,
result => format_msg(Re)
}, case Login of
all -> #{all => '$all'};
_ -> maps:from_list([Login])
end).
delete(#{clientid := Clientid, topic := Topic}, _) ->
return(emqx_acl_mnesia_cli:remove_acl({clientid, urldecode(Clientid)}, urldecode(Topic)));
delete(#{username := Username, topic := Topic}, _) ->
return(emqx_acl_mnesia_cli:remove_acl({username, urldecode(Username)}, urldecode(Topic)));
delete(#{topic := Topic}, _) ->
return(emqx_acl_mnesia_cli:remove_acl(all, urldecode(Topic))).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Interval Funcs %% Interval Funcs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
format({{clientid, Clientid}, Topic, Action, Access, _CreatedAt}) ->
format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) -> #{clientid => Clientid, topic => Topic, action => Action, access => Access};
#{login => Login, topic => Topic, action => Action, allow => Allow }; format({{username, Username}, Topic, Action, Access, _CreatedAt}) ->
#{username => Username, topic => Topic, action => Action, access => Access};
format([]) -> format({all, Topic, Action, Access, _CreatedAt}) ->
#{}; #{all => '$all', topic => Topic, action => Action, access => Access};
format(List) when is_list(List) ->
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(List, []).
format([#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow} | List], ReList) -> format([L | List], Relist) ->
format(List, [ format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) | ReList]); format(List, [format(L) | Relist]);
format([], ReList) -> ReList. format([], ReList) -> lists:reverse(ReList).
validate([], []) -> validate([], []) ->
ok; ok;
@ -114,8 +194,18 @@ validate([K|Keys], [V|Values]) ->
false -> {error, K}; false -> {error, K};
true -> validate(Keys, Values) true -> validate(Keys, Values)
end. end.
do_validation(login, all) ->
do_validation(login, V) when is_binary(V) true;
do_validation(login, {clientid, V}) when is_binary(V)
andalso byte_size(V) > 0->
true;
do_validation(login, {username, V}) when is_binary(V)
andalso byte_size(V) > 0->
true;
do_validation(clientid, V) when is_binary(V)
andalso byte_size(V) > 0 ->
true;
do_validation(username, V) when is_binary(V)
andalso byte_size(V) > 0 -> andalso byte_size(V) > 0 ->
true; true;
do_validation(topic, V) when is_binary(V) do_validation(topic, V) when is_binary(V)
@ -126,7 +216,7 @@ do_validation(action, V) when is_binary(V) ->
true -> true; true -> true;
false -> false false -> false
end; end;
do_validation(allow, V) when is_boolean(V) -> do_validation(access, V) when V =:= <<"allow">> orelse V =:= <<"deny">> ->
true; true;
do_validation(_, _) -> do_validation(_, _) ->
false. false.
@ -145,4 +235,3 @@ urldecode(S) ->
urldecode(S) -> urldecode(S) ->
http_uri:decode(S). http_uri:decode(S).
-endif. -endif.

View File

@ -0,0 +1,198 @@
%%--------------------------------------------------------------------
%% 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_cli).
-include("emqx_auth_mnesia.hrl").
-include_lib("emqx_libs/include/logger.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-define(TABLE, emqx_acl).
%% Acl APIs
-export([ add_acl/4
, lookup_acl/1
, all_acls/0
, all_acls/1
, remove_acl/2
]).
-export([cli/1]).
-export([comparing/2]).
%%--------------------------------------------------------------------
%% Acl API
%%--------------------------------------------------------------------
%% @doc Add Acls
-spec(add_acl(login() |all, emqx_topic:topic(), pub | sub| pubsub, allow | deny) -> ok | {error, any()}).
add_acl(Login, Topic, Action, Access) ->
Acls = #?TABLE{
filter = {Login, Topic},
action = Action,
access = Access,
created_at = erlang:system_time(millisecond)
},
ret(mnesia:transaction(fun mnesia:write/1, [Acls])).
%% @doc Lookup acl by login
-spec(lookup_acl(login() | all) -> list()).
lookup_acl(undefined) -> [];
lookup_acl(Login) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {Filter, ACLTopic}, Action, Access, CreatedAt})
when Filter =:= Login -> {Filter, ACLTopic, Action, Access, CreatedAt} end),
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)).
%% @doc Remove acl
-spec(remove_acl(login() | all, emqx_topic:topic()) -> ok | {error, any()}).
remove_acl(Login, Topic) ->
ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, {Login, Topic}}])).
%% @doc All logins
-spec(all_acls() -> list()).
all_acls() ->
all_acls(clientid) ++
all_acls(username) ++
all_acls(all).
all_acls(clientid) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {{clientid, Clientid}, Topic}, Action, Access, CreatedAt}) -> {{clientid, Clientid}, Topic, Action, Access, CreatedAt} end),
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec));
all_acls(username) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {{username, Username}, Topic}, Action, Access, CreatedAt}) -> {{username, Username}, Topic, Action, Access, CreatedAt} end),
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec));
all_acls(all) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {all, Topic}, Action, Access, CreatedAt}) -> {all, Topic, Action, Access, CreatedAt} end),
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
comparing({_, _, _, _, CreatedAt1},
{_, _, _, _, CreatedAt2}) ->
CreatedAt1 >= CreatedAt2.
ret({atomic, ok}) -> ok;
ret({aborted, Error}) -> {error, Error}.
validate(action, "pub") -> true;
validate(action, "sub") -> true;
validate(action, "pubsub") -> true;
validate(access, "allow") -> true;
validate(access, "deny") -> true;
validate(_, _) -> false.
%%--------------------------------------------------------------------
%% ACL Cli
%%--------------------------------------------------------------------
cli(["list"]) ->
[ begin
case Filter of
{clientid, Clientid} ->
emqx_ctl:print("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",[Clientid, Topic, Action, Access]);
{username, Username} ->
emqx_ctl:print("Acl(username = ~p topic = ~p action = ~p access = ~p)~n",[Username, Topic, Action, Access]);
all ->
emqx_ctl:print("Acl($all topic = ~p action = ~p access = ~p)~n",[Topic, Action, Access])
end
end || {Filter, Topic, Action, Access, _} <- all_acls()];
cli(["list", "clientid"]) ->
[emqx_ctl:print("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",[Clientid, Topic, Action, Access])
|| {{clientid, Clientid}, Topic, Action, Access, _} <- all_acls(clientid) ];
cli(["list", "username"]) ->
[emqx_ctl:print("Acl(username = ~p topic = ~p action = ~p access = ~p)~n",[Username, Topic, Action, Access])
|| {{username, Username}, Topic, Action, Access, _} <- all_acls(username) ];
cli(["list", "_all"]) ->
[emqx_ctl:print("Acl($all topic = ~p action = ~p access = ~p)~n",[Topic, Action, Access])
|| {all, Topic, Action, Access, _} <- all_acls(all) ];
cli(["add", "clientid", Clientid, Topic, Action, Access]) ->
case validate(action, Action) andalso validate(access, Access) of
true ->
case add_acl({clientid, iolist_to_binary(Clientid)}, iolist_to_binary(Topic), list_to_existing_atom(Action), list_to_existing_atom(Access)) of
ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end;
_ ->
emqx_ctl:print("Error: Input is illegal~n")
end;
cli(["add", "username", Username, Topic, Action, Access]) ->
case validate(action, Action) andalso validate(access, Access) of
true ->
case add_acl({username, iolist_to_binary(Username)}, iolist_to_binary(Topic), list_to_existing_atom(Action), list_to_existing_atom(Access)) of
ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end;
_ ->
emqx_ctl:print("Error: Input is illegal~n")
end;
cli(["add", "_all", Topic, Action, Access]) ->
case validate(action, Action) andalso validate(access, Access) of
true ->
case add_acl(all, iolist_to_binary(Topic), list_to_existing_atom(Action), list_to_existing_atom(Access)) of
ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end;
_ ->
emqx_ctl:print("Error: Input is illegal~n")
end;
cli(["show", "clientid", Clientid]) ->
[emqx_ctl:print("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",[NClientid, Topic, Action, Access])
|| {{clientid, NClientid}, Topic, Action, Access, _} <- lookup_acl({clientid, iolist_to_binary(Clientid)}) ];
cli(["show", "username", Username]) ->
[emqx_ctl:print("Acl(username = ~p topic = ~p action = ~p access = ~p)~n",[NUsername, Topic, Action, Access])
|| {{username, NUsername}, Topic, Action, Access, _} <- lookup_acl({username, iolist_to_binary(Username)}) ];
cli(["del", "clientid", Clientid, Topic])->
case remove_acl({clientid, iolist_to_binary(Clientid)}, iolist_to_binary(Topic)) of
ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end;
cli(["del", "username", Username, Topic])->
case remove_acl({username, iolist_to_binary(Username)}, iolist_to_binary(Topic)) of
ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end;
cli(["del", "_all", Topic])->
case remove_acl(all, iolist_to_binary(Topic)) of
ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end;
cli(_) ->
emqx_ctl:usage([ {"acl list clientid","List clientid acls"}
, {"acl list username","List username acls"}
, {"acl list _all","List $all acls"}
, {"acl show clientid <Clientid>", "Lookup clientid acl detail"}
, {"acl show username <Username>", "Lookup username acl detail"}
, {"acl aad clientid <Clientid> <Topic> <Action> <Access>", "Add clientid acl"}
, {"acl add Username <Username> <Topic> <Action> <Access>", "Add username acl"}
, {"acl add _all <Topic> <Action> <Access>", "Add $all acl"}
, {"acl del clientid <Clientid> <Topic>", "Delete clientid acl"}
, {"acl del username <Username> <Topic>", "Delete username acl"}
, {"acl del _all, <Topic>", "Delete $all acl"}
]).

View File

@ -1,9 +1,9 @@
{application, emqx_auth_mnesia, {application, emqx_auth_mnesia,
[{description, "EMQ X Authentication with Mnesia"}, [{description, "EMQ X Authentication with Mnesia"},
{vsn, "5.0.0"}, % strict semver, bump manually! {vsn, "4.3.0"}, % strict semver, bump manually
{modules, []}, {modules, []},
{registered, []}, {registered, []},
{applications, [kernel,stdlib,mnesia,emqx_libs]}, {applications, [kernel,stdlib,mnesia]},
{mod, {emqx_auth_mnesia_app,[]}}, {mod, {emqx_auth_mnesia_app,[]}},
{env, []}, {env, []},
{licenses, ["Apache-2.0"]}, {licenses, ["Apache-2.0"]},

View File

@ -22,6 +22,9 @@
-include_lib("emqx_libs/include/logger.hrl"). -include_lib("emqx_libs/include/logger.hrl").
-include_lib("emqx_libs/include/types.hrl"). -include_lib("emqx_libs/include/types.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-define(TABLE, emqx_user).
%% Auth callbacks %% Auth callbacks
-export([ init/1 -export([ init/1
, register_metrics/0 , register_metrics/0
@ -29,48 +32,53 @@
, description/0 , description/0
]). ]).
init(DefaultUsers) -> init(#{clientid_list := ClientidList, username_list := UsernameList}) ->
ok = ekka_mnesia:create_table(emqx_user, [ ok = ekka_mnesia:create_table(emqx_user, [
{disc_copies, [node()]}, {disc_copies, [node()]},
{attributes, record_info(fields, emqx_user)}, {attributes, record_info(fields, emqx_user)},
{storage_properties, [{ets, [{read_concurrency, true}]}]}]), {storage_properties, [{ets, [{read_concurrency, true}]}]}]),
ok = lists:foreach(fun add_default_user/1, DefaultUsers), [ add_default_user({{clientid, iolist_to_binary(Clientid)}, iolist_to_binary(Password)})
|| {Clientid, Password} <- ClientidList],
[ add_default_user({{username, iolist_to_binary(Username)}, iolist_to_binary(Password)})
|| {Username, Password} <- UsernameList],
ok = ekka_mnesia:copy_table(emqx_user, disc_copies). ok = ekka_mnesia:copy_table(emqx_user, disc_copies).
%% @private %% @private
add_default_user({Login, Password, IsSuperuser}) -> add_default_user({Login, Password}) when is_tuple(Login) ->
emqx_auth_mnesia_cli:add_user(iolist_to_binary(Login), iolist_to_binary(Password), IsSuperuser). emqx_auth_mnesia_cli:add_user(Login, Password).
-spec(register_metrics() -> ok). -spec(register_metrics() -> ok).
register_metrics() -> register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
check(ClientInfo = #{password := Password}, AuthResult, #{hash_type := HashType, key_as := As}) -> check(ClientInfo = #{ clientid := Clientid
Login = maps:get(As, ClientInfo), , password := NPassword
case emqx_auth_mnesia_cli:lookup_user(Login) of }, AuthResult, #{hash_type := HashType}) ->
Username = maps:get(username, ClientInfo, undefined),
MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, X }, Password, InterTime}) when X =:= Clientid-> Password;
({?TABLE, {username, X }, Password, InterTime}) when X =:= Username andalso X =/= undefined -> Password
end),
case ets:select(?TABLE, MatchSpec) of
[] -> [] ->
emqx_metrics:inc(?AUTH_METRICS(ignore)), emqx_metrics:inc(?AUTH_METRICS(ignore)),
ok; ok;
[User] -> List ->
case emqx_passwd:check_pass({User#emqx_user.password, Password}, HashType) of case [ Hash || <<Salt:4/binary, Hash/binary>> <- lists:sort(fun emqx_auth_mnesia_cli:comparing/2, List),
ok -> Hash =:= hash(NPassword, Salt, HashType)
emqx_metrics:inc(?AUTH_METRICS(success)), ] of
{stop, AuthResult#{is_superuser => is_superuser(User), [] ->
anonymous => false, ?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [ClientInfo]),
auth_result => success}};
{error, Reason} ->
?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [Reason]),
emqx_metrics:inc(?AUTH_METRICS(failure)), emqx_metrics:inc(?AUTH_METRICS(failure)),
{stop, AuthResult#{auth_result => password_error, anonymous => false}} {stop, AuthResult#{anonymous => false, auth_result => password_error}};
_ ->
emqx_metrics:inc(?AUTH_METRICS(success)),
{stop, AuthResult#{anonymous => false, auth_result => success}}
end end
end. end.
description() -> "Authentication with Mnesia". description() -> "Authentication with Mnesia".
%%-------------------------------------------------------------------- hash(undefined, SaltBin, HashType) ->
%% Internal functions hash(<<>>, SaltBin, HashType);
%%-------------------------------------------------------------------- hash(Password, SaltBin, HashType) ->
is_superuser(#emqx_user{is_superuser = true}) -> emqx_passwd:hash(HashType, <<SaltBin/binary, Password/binary>>).
true;
is_superuser(_) ->
false.

View File

@ -17,100 +17,206 @@
-module(emqx_auth_mnesia_api). -module(emqx_auth_mnesia_api).
-include_lib("stdlib/include/qlc.hrl"). -include_lib("stdlib/include/qlc.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-define(TABLE, emqx_user).
-import(proplists, [get_value/2]). -import(proplists, [get_value/2]).
-import(minirest, [return/1]). -import(minirest, [return/1]).
-export([paginate/5]).
-rest_api(#{name => list_emqx_user, -export([ list_clientid/2
method => 'GET', , lookup_clientid/2
path => "/mqtt_user", , add_clientid/2
func => list, , update_clientid/2
descr => "List available mnesia in the cluster" , delete_clientid/2
}).
-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]). -rest_api(#{name => list_clientid,
method => 'GET',
path => "/auth_clientid",
func => list_clientid,
descr => "List available clientid in the cluster"
}).
list(_Bindings, Params) -> -rest_api(#{name => lookup_clientid,
return({ok, paginate(emqx_user, Params, fun format/1)}). method => 'GET',
path => "/auth_clientid/:bin:clientid",
func => lookup_clientid,
descr => "Lookup clientid in the cluster"
}).
lookup(#{login := Login}, _Params) -> -rest_api(#{name => add_clientid,
return({ok, format(emqx_auth_mnesia_cli:lookup_user(urldecode(Login)))}). method => 'POST',
path => "/auth_clientid",
func => add_clientid,
descr => "Add clientid in the cluster"
}).
add(_Bindings, Params) -> -rest_api(#{name => update_clientid,
method => 'PUT',
path => "/auth_clientid/:bin:clientid",
func => update_clientid,
descr => "Update clientid in the cluster"
}).
-rest_api(#{name => delete_clientid,
method => 'DELETE',
path => "/auth_clientid/:bin:clientid",
func => delete_clientid,
descr => "Delete clientid in the cluster"
}).
-export([ list_username/2
, lookup_username/2
, add_username/2
, update_username/2
, delete_username/2
]).
-rest_api(#{name => list_username,
method => 'GET',
path => "/auth_username",
func => list_username,
descr => "List available username in the cluster"
}).
-rest_api(#{name => lookup_username,
method => 'GET',
path => "/auth_username/:bin:username",
func => lookup_username,
descr => "Lookup username in the cluster"
}).
-rest_api(#{name => add_username,
method => 'POST',
path => "/auth_username",
func => add_username,
descr => "Add username in the cluster"
}).
-rest_api(#{name => update_username,
method => 'PUT',
path => "/auth_username/:bin:username",
func => update_username,
descr => "Update username in the cluster"
}).
-rest_api(#{name => delete_username,
method => 'DELETE',
path => "/auth_username/:bin:username",
func => delete_username,
descr => "Delete username in the cluster"
}).
%%------------------------------------------------------------------------------
%% Auth Clientid Api
%%------------------------------------------------------------------------------
list_clientid(_Bindings, Params) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) -> {?TABLE, {clientid, Clientid}, Password, CreatedAt} end),
return({ok, paginate(?TABLE, MatchSpec, Params, fun emqx_auth_mnesia_cli:comparing/2, fun({?TABLE, {clientid, X}, _, _}) -> #{clientid => X} end)}).
lookup_clientid(#{clientid := Clientid}, _Params) ->
return({ok, format(emqx_auth_mnesia_cli:lookup_user({clientid, urldecode(Clientid)}))}).
add_clientid(_Bindings, Params) ->
[ P | _] = Params, [ P | _] = Params,
case is_list(P) of case is_list(P) of
true -> return(add_user(Params, [])); true -> return(do_add_clientid(Params, []));
false -> return(add_user([Params], [])) false ->
Re = do_add_clientid(Params),
case Re of
ok -> return(ok);
<<"ok">> -> return(ok);
_ -> return({error, format_msg(Re)})
end
end. end.
add_user([ Params | ParamsN ], ReList ) -> do_add_clientid([ Params | ParamsN ], ReList ) ->
Login = urldecode(get_value(<<"login">>, Params)), Clientid = urldecode(get_value(<<"clientid">>, Params)),
Password = urldecode(get_value(<<"password">>, Params)), do_add_clientid(ParamsN, [{Clientid, format_msg(do_add_clientid(Params))} | ReList]);
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) -> do_add_clientid([], ReList) ->
{ok, ReList}. {ok, ReList}.
update(#{login := Login}, Params) -> do_add_clientid(Params) ->
Clientid = urldecode(get_value(<<"clientid">>, Params)),
Password = urldecode(get_value(<<"password">>, Params)),
Login = {clientid, Clientid},
case validate([login, password], [Login, Password]) of
ok ->
emqx_auth_mnesia_cli:add_user(Login, Password);
Err -> Err
end.
update_clientid(#{clientid := Clientid}, Params) ->
Password = get_value(<<"password">>, Params), Password = get_value(<<"password">>, Params),
IsSuperuser = get_value(<<"is_superuser">>, Params), case validate([password], [Password]) of
case validate([password, is_superuser], [Password, IsSuperuser]) of ok -> return(emqx_auth_mnesia_cli:update_user({clientid, urldecode(Clientid)}, urldecode(Password)));
ok -> return(emqx_auth_mnesia_cli:update_user(urldecode(Login), urldecode(Password), IsSuperuser));
Err -> return(Err) Err -> return(Err)
end. end.
delete(#{login := Login}, _) -> delete_clientid(#{clientid := Clientid}, _) ->
return(emqx_auth_mnesia_cli:remove_user(urldecode(Login))). return(emqx_auth_mnesia_cli:remove_user({clientid, urldecode(Clientid)})).
%%------------------------------------------------------------------------------
%% Auth Username Api
%%------------------------------------------------------------------------------
list_username(_Bindings, Params) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {username, Username}, Password, CreatedAt}) -> {?TABLE, {username, Username}, Password, CreatedAt} end),
return({ok, paginate(?TABLE, MatchSpec, Params, fun emqx_auth_mnesia_cli:comparing/2, fun({?TABLE, {username, X}, _, _}) -> #{username => X} end)}).
lookup_username(#{username := Username}, _Params) ->
return({ok, format(emqx_auth_mnesia_cli:lookup_user({username, urldecode(Username)}))}).
add_username(_Bindings, Params) ->
[ P | _] = Params,
case is_list(P) of
true -> return(do_add_username(Params, []));
false ->
case do_add_username(Params) of
ok -> return(ok);
<<"ok">> -> return(ok);
Error -> return({error, format_msg(Error)})
end
end.
do_add_username([ Params | ParamsN ], ReList ) ->
Username = urldecode(get_value(<<"username">>, Params)),
do_add_username(ParamsN, [{Username, format_msg(do_add_username(Params))} | ReList]);
do_add_username([], ReList) ->
{ok, ReList}.
do_add_username(Params) ->
Username = urldecode(get_value(<<"username">>, Params)),
Password = urldecode(get_value(<<"password">>, Params)),
Login = {username, Username},
case validate([login, password], [Login, Password]) of
ok ->
emqx_auth_mnesia_cli:add_user(Login, Password);
Err -> Err
end.
update_username(#{username := Username}, Params) ->
Password = get_value(<<"password">>, Params),
case validate([password], [Password]) of
ok -> return(emqx_auth_mnesia_cli:update_user({username, urldecode(Username)}, urldecode(Password)));
Err -> return(Err)
end.
delete_username(#{username := Username}, _) ->
return(emqx_auth_mnesia_cli:remove_user({username, urldecode(Username)})).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Paging Query %% Paging Query
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
paginate(Tables, Params, RowFun) -> paginate(Tables, MatchSpec, Params, ComparingFun, RowFun) ->
Qh = query_handle(Tables), Qh = query_handle(Tables, MatchSpec),
Count = count(Tables), Count = count(Tables, MatchSpec),
Page = page(Params), Page = page(Params),
Limit = limit(Params), Limit = limit(Params),
Cursor = qlc:cursor(Qh), Cursor = qlc:cursor(Qh),
@ -121,21 +227,28 @@ paginate(Tables, Params, RowFun) ->
Rows = qlc:next_answers(Cursor, Limit), Rows = qlc:next_answers(Cursor, Limit),
qlc:delete_cursor(Cursor), qlc:delete_cursor(Cursor),
#{meta => #{page => Page, limit => Limit, count => Count}, #{meta => #{page => Page, limit => Limit, count => Count},
data => [RowFun(Row) || Row <- Rows]}. data => [RowFun(Row) || Row <- lists:sort(ComparingFun, Rows)]}.
query_handle(Table) when is_atom(Table) -> query_handle(Table, MatchSpec) when is_atom(Table) ->
qlc:q([R|| R <- ets:table(Table)]); Options = {traverse, {select, MatchSpec}},
query_handle([Table]) when is_atom(Table) -> qlc:q([R|| R <- ets:table(Table, Options)]);
qlc:q([R|| R <- ets:table(Table)]); query_handle([Table], MatchSpec) when is_atom(Table) ->
query_handle(Tables) -> Options = {traverse, {select, MatchSpec}},
qlc:append([qlc:q([E || E <- ets:table(T)]) || T <- Tables]). qlc:q([R|| R <- ets:table(Table, Options)]);
query_handle(Tables, MatchSpec) ->
Options = {traverse, {select, MatchSpec}},
qlc:append([qlc:q([E || E <- ets:table(T, Options)]) || T <- Tables]).
count(Table) when is_atom(Table) -> count(Table, MatchSpec) when is_atom(Table) ->
ets:info(Table, size); [{MatchPattern, Where, _Re}] = MatchSpec,
count([Table]) when is_atom(Table) -> NMatchSpec = [{MatchPattern, Where, [true]}],
ets:info(Table, size); ets:select_count(Table, NMatchSpec);
count(Tables) -> count([Table], MatchSpec) when is_atom(Table) ->
lists:sum([count(T) || T <- Tables]). [{MatchPattern, Where, _Re}] = MatchSpec,
NMatchSpec = [{MatchPattern, Where, [true]}],
ets:select_count(Table, NMatchSpec);
count(Tables, MatchSpec) ->
lists:sum([count(T, MatchSpec) || T <- Tables]).
page(Params) -> page(Params) ->
binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)). binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)).
@ -146,24 +259,28 @@ limit(Params) ->
Size -> binary_to_integer(Size) Size -> binary_to_integer(Size)
end. end.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Interval Funcs %% Interval Funcs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
format({emqx_user, Login, Password, IsSuperuser}) -> format({?TABLE, {clientid, ClientId}, Password, _InterTime}) ->
#{login => Login, #{clientid => ClientId,
password => Password, password => Password};
is_superuser => IsSuperuser};
format({?TABLE, {username, Username}, Password, _InterTime}) ->
#{username => Username,
password => Password};
format([{?TABLE, {clientid, ClientId}, Password, _InterTime}]) ->
#{clientid => ClientId,
password => Password};
format([{?TABLE, {username, Username}, Password, _InterTime}]) ->
#{username => Username,
password => Password};
format([]) -> format([]) ->
#{}; #{}.
format([{emqx_user, Login, Password, IsSuperuser}]) ->
#{login => Login,
password => Password,
is_superuser => IsSuperuser}.
validate([], []) -> validate([], []) ->
ok; ok;
@ -173,14 +290,15 @@ validate([K|Keys], [V|Values]) ->
true -> validate(Keys, Values) true -> validate(Keys, Values)
end. end.
do_validation(login, V) when is_binary(V) do_validation(login, {clientid, V}) when is_binary(V)
andalso byte_size(V) > 0 ->
true;
do_validation(login, {username, V}) when is_binary(V)
andalso byte_size(V) > 0 -> andalso byte_size(V) > 0 ->
true; true;
do_validation(password, V) when is_binary(V) do_validation(password, V) when is_binary(V)
andalso byte_size(V) > 0 -> andalso byte_size(V) > 0 ->
true; true;
do_validation(is_superuser, V) when is_boolean(V) ->
true;
do_validation(_, _) -> do_validation(_, _) ->
false. false.

View File

@ -34,8 +34,10 @@
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
{ok, Sup} = emqx_auth_mnesia_sup:start_link(), {ok, Sup} = emqx_auth_mnesia_sup:start_link(),
emqx_ctl:register_command('mqtt-user', {emqx_auth_mnesia_cli, auth_cli}, []), emqx_ctl:register_command(clientid, {emqx_auth_mnesia_cli, auth_clientid_cli}, []),
emqx_ctl:register_command('mqtt-acl', {emqx_auth_mnesia_cli, acl_cli}, []), emqx_ctl:register_command(username, {emqx_auth_mnesia_cli, auth_username_cli}, []),
emqx_ctl:register_command(user, {emqx_auth_mnesia_cli, auth_username_cli}, []),
emqx_ctl:register_command(acl, {emqx_acl_mnesia_cli, cli}, []),
load_auth_hook(), load_auth_hook(),
load_acl_hook(), load_acl_hook(),
{ok, Sup}. {ok, Sup}.
@ -43,28 +45,26 @@ start(_StartType, _StartArgs) ->
prep_stop(State) -> prep_stop(State) ->
emqx:unhook('client.authenticate', fun emqx_auth_mnesia:check/3), emqx:unhook('client.authenticate', fun emqx_auth_mnesia:check/3),
emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5), emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5),
emqx_ctl:unregister_command('mqtt-user'), emqx_ctl:unregister_command(clientid),
emqx_ctl:unregister_command('mqtt-acl'), emqx_ctl:unregister_command(username),
emqx_ctl:unregister_command(user),
emqx_ctl:unregister_command(acl),
State. State.
stop(_State) -> stop(_State) ->
ok. ok.
load_auth_hook() -> load_auth_hook() ->
DefaultUsers = application:get_env(?APP, userlist, []), ClientidList = application:get_env(?APP, clientid_list, []),
ok = emqx_auth_mnesia:init(DefaultUsers), UsernameList = application:get_env(?APP, username_list, []),
ok = emqx_auth_mnesia:init(#{clientid_list => ClientidList, username_list => UsernameList}),
ok = emqx_auth_mnesia:register_metrics(), ok = emqx_auth_mnesia:register_metrics(),
Params = #{ Params = #{
hash_type => application:get_env(emqx_auth_mnesia, hash_type, sha256), 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]). emqx:hook('client.authenticate', fun emqx_auth_mnesia:check/3, [Params]).
load_acl_hook() -> load_acl_hook() ->
ok = emqx_acl_mnesia:init(), ok = emqx_acl_mnesia:init(),
ok = emqx_acl_mnesia:register_metrics(), ok = emqx_acl_mnesia:register_metrics(),
Params = #{ emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [#{}]).
key_as => application:get_env(emqx_auth_mnesia, as, username)
},
emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [Params]).

View File

@ -18,31 +18,32 @@
-include("emqx_auth_mnesia.hrl"). -include("emqx_auth_mnesia.hrl").
-include_lib("emqx_libs/include/logger.hrl"). -include_lib("emqx_libs/include/logger.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-define(TABLE, emqx_user). -define(TABLE, emqx_user).
%% Auth APIs %% Auth APIs
-export([ add_user/3 -export([ add_user/2
, update_user/3 , update_user/2
, remove_user/1 , remove_user/1
, lookup_user/1 , lookup_user/1
, all_users/0 , all_users/0
]). , all_users/1
%% Acl APIs
-export([ add_acl/4
, remove_acl/2
, lookup_acl/1
, all_acls/0
]). ]).
%% Cli %% Cli
-export([ auth_cli/1 -export([ auth_clientid_cli/1
, acl_cli/1]). , auth_username_cli/1
]).
%% Helper
-export([comparing/2]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Auth APIs %% Auth APIs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% @doc Add User %% @doc Add User
-spec(add_user(binary(), binary(), atom()) -> ok | {error, any()}). -spec(add_user(tuple(), binary()) -> ok | {error, any()}).
add_user(Login, Password, IsSuperuser) -> add_user(Login, Password) ->
User = #emqx_user{login = Login, password = encrypted_data(Password), is_superuser = IsSuperuser}, User = #emqx_user{login = Login, password = encrypted_data(Password), created_at = erlang:system_time(millisecond)},
ret(mnesia:transaction(fun insert_user/1, [User])). ret(mnesia:transaction(fun insert_user/1, [User])).
insert_user(User = #emqx_user{login = Login}) -> insert_user(User = #emqx_user{login = Login}) ->
@ -52,30 +53,31 @@ insert_user(User = #emqx_user{login = Login}) ->
end. end.
%% @doc Update User %% @doc Update User
-spec(update_user(binary(), binary(), atom()) -> ok | {error, any()}). -spec(update_user(tuple(), binary()) -> ok | {error, any()}).
update_user(Login, NewPassword, IsSuperuser) -> update_user(Login, NewPassword) ->
User = #emqx_user{login = Login, password = encrypted_data(NewPassword), is_superuser = IsSuperuser}, User = #emqx_user{login = Login, password = encrypted_data(NewPassword)},
ret(mnesia:transaction(fun do_update_user/1, [User])). ret(mnesia:transaction(fun do_update_user/1, [User])).
do_update_user(User = #emqx_user{login = Login}) -> do_update_user(User = #emqx_user{login = Login}) ->
case mnesia:read(?TABLE, Login) of case mnesia:read(?TABLE, Login) of
[_|_] -> mnesia:write(User); [{?TABLE, Login, _, CreateAt}] -> mnesia:write(User#emqx_user{created_at = CreateAt});
[] -> mnesia:abort(noexisted) [] -> mnesia:abort(noexisted)
end. end.
%% @doc Lookup user by login %% @doc Lookup user by login
-spec(lookup_user(binary()) -> list()). -spec(lookup_user(tuple()) -> list()).
lookup_user(undefined) -> []; lookup_user(undefined) -> [];
lookup_user(Login) -> lookup_user(Login) ->
case mnesia:dirty_read(?TABLE, Login) of case mnesia:dirty_read(?TABLE, Login) of
{error, Reason} -> {error, Reason} ->
?LOG(error, "[Mnesia] do_check_user error: ~p~n", [Reason]), ?LOG(error, "[Mnesia] do_check_user error: ~p~n", [Reason]),
[]; [];
Re -> Re Re ->
lists:sort(fun comparing/2, Re)
end. end.
%% @doc Remove user %% @doc Remove user
-spec(remove_user(binary()) -> ok | {error, any()}). -spec(remove_user(tuple()) -> ok | {error, any()}).
remove_user(Login) -> remove_user(Login) ->
ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, Login}])). ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, Login}])).
@ -83,111 +85,97 @@ remove_user(Login) ->
-spec(all_users() -> list()). -spec(all_users() -> list()).
all_users() -> mnesia:dirty_all_keys(?TABLE). all_users() -> mnesia:dirty_all_keys(?TABLE).
%%-------------------------------------------------------------------- all_users(clientid) ->
%% Acl API MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) -> {?TABLE, {clientid, Clientid}, Password, CreatedAt} end),
%%-------------------------------------------------------------------- lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec));
%% @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).
all_users(username) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {username, Username}, Password, CreatedAt}) -> {?TABLE, {username, Username}, Password, CreatedAt} end),
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
comparing({?TABLE, _, _, CreatedAt1},
{?TABLE, _, _, CreatedAt2}) ->
CreatedAt1 >= CreatedAt2.
ret({atomic, ok}) -> ok; ret({atomic, ok}) -> ok;
ret({aborted, Error}) -> {error, Error}. ret({aborted, Error}) -> {error, Error}.
encrypted_data(Password) -> encrypted_data(Password) ->
HashType = application:get_env(emqx_auth_mnesia, hash_type, sha256), HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256),
emqx_passwd:hash(HashType, Password). 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>>.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Auth APIs %% Auth Clientid Cli
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% User auth_clientid_cli(["list"]) ->
auth_cli(["add", Login, Password, IsSuperuser]) -> [emqx_ctl:print("~s~n", [ClientId]) || {?TABLE, {clientid, ClientId}, _Password, _CreatedAt} <- all_users(clientid)];
case add_user(iolist_to_binary(Login), iolist_to_binary(Password), IsSuperuser) of
auth_clientid_cli(["add", ClientId, Password]) ->
case add_user({clientid, iolist_to_binary(ClientId)}, iolist_to_binary(Password)) of
ok -> emqx_ctl:print("ok~n"); ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end; end;
auth_cli(["update", Login, NewPassword, IsSuperuser]) -> auth_clientid_cli(["update", ClientId, NewPassword]) ->
case update_user(iolist_to_binary(Login), iolist_to_binary(NewPassword), IsSuperuser) of case update_user({clientid, iolist_to_binary(ClientId)}, iolist_to_binary(NewPassword)) of
ok -> emqx_ctl:print("ok~n"); ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end; end;
auth_cli(["del", Login]) -> auth_clientid_cli(["del", ClientId]) ->
case remove_user(iolist_to_binary(Login)) of case remove_user({clientid, iolist_to_binary(ClientId)}) of
ok -> emqx_ctl:print("ok~n"); ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end; end;
auth_cli(["show", P]) -> auth_clientid_cli(_) ->
[emqx_ctl:print("User(login = ~p is_super = ~p)~n", [Login, IsSuperuser]) emqx_ctl:usage([{"clientid list", "List clientid auth rules"},
|| {_, Login, _Password, IsSuperuser} <- lookup_user(iolist_to_binary(P))]; {"clientid add <Username> <Password>", "Add clientid auth rule"},
{"clientid update <Username> <NewPassword>", "Update clientid auth rule"},
{"clientid del <Username>", "Delete clientid auth rule"}]).
auth_cli(["list"]) -> %%--------------------------------------------------------------------
[emqx_ctl:print("User(login = ~p)~n",[E]) %% Auth Username Cli
|| E <- all_users()]; %%--------------------------------------------------------------------
auth_cli(_) -> auth_username_cli(["list"]) ->
emqx_ctl:usage([{"mqtt-user add <Login> <Password> <IsSuper>", "Add user"}, [emqx_ctl:print("~s~n", [Username]) || {?TABLE, {username, Username}, _Password, _CreatedAt}<- all_users(username)];
{"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 auth_username_cli(["add", Username, Password]) ->
acl_cli(["add", Login, Topic, Action, Allow]) -> case add_user({username, iolist_to_binary(Username)}, iolist_to_binary(Password)) of
case add_acl(iolist_to_binary(Login), iolist_to_binary(Topic), iolist_to_binary(Action), Allow) of
ok -> emqx_ctl:print("ok~n"); ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end; end;
acl_cli(["del", Login, Topic])-> auth_username_cli(["update", Username, NewPassword]) ->
case remove_acl(iolist_to_binary(Login), iolist_to_binary(Topic)) of case update_user({username, iolist_to_binary(Username)}, iolist_to_binary(NewPassword)) of
ok -> emqx_ctl:print("ok~n"); ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end; end;
acl_cli(["show", P]) -> auth_username_cli(["del", Username]) ->
[emqx_ctl:print("Acl(login = ~p topic = ~p action = ~p allow = ~p)~n",[Login, Topic, Action, Allow]) case remove_user({username, iolist_to_binary(Username)}) of
|| {_, Login, Topic, Action, Allow} <- lookup_acl(iolist_to_binary(P)) ]; ok -> emqx_ctl:print("ok~n");
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
end;
acl_cli(["list"]) -> auth_username_cli(_) ->
[emqx_ctl:print("Acl(login = ~p)~n",[E]) emqx_ctl:usage([{"users list", "List username auth rules"},
|| E <- all_acls() ]; {"users add <Username> <Password>", "Add username auth rule"},
{"users update <Username> <NewPassword>", "Update username auth rule"},
acl_cli(_) -> {"users del <Username>", "Delete username auth rule"}]).
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"}]).

View File

@ -76,154 +76,41 @@ set_special_configs(_App) ->
t_management(_Config) -> t_management(_Config) ->
clean_all_acls(), clean_all_acls(),
?assertEqual("Acl with Mnesia", emqx_acl_mnesia:description()), ?assertEqual("Acl with Mnesia", emqx_acl_mnesia:description()),
?assertEqual([], emqx_auth_mnesia_cli:all_acls()), ?assertEqual([], emqx_acl_mnesia_cli:all_acls()),
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true), ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>, sub, allow),
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true), ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/+">>, pub, deny),
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/C">>, <<"pubsub">>, true), ok = emqx_acl_mnesia_cli:add_acl({username, <<"test_username">>}, <<"topic/%u">>, sub, deny),
ok = emqx_acl_mnesia_cli:add_acl({username, <<"test_username">>}, <<"topic/+">>, pub, allow),
ok = emqx_acl_mnesia_cli:add_acl(all, <<"#">>, pubsub, deny),
?assertEqual([{emqx_acl,<<"test_username">>,<<"Topic/A">>,<<"sub">>, true}, ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({clientid, <<"test_clientid">>}))),
{emqx_acl,<<"test_username">>,<<"Topic/B">>,<<"pub">>, true}, ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({username, <<"test_username">>}))),
{emqx_acl,<<"test_username">>,<<"Topic/C">>,<<"pubsub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"test_username">>)), ?assertEqual(1, length(emqx_acl_mnesia_cli:lookup_acl(all))),
ok = emqx_auth_mnesia_cli:remove_acl(<<"test_username">>, <<"Topic/A">>), ?assertEqual(5, length(emqx_acl_mnesia_cli:all_acls())),
?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">>}, User1 = #{zone => external, clientid => <<"test_clientid">>},
User2 = #{zone => external, clientid => <<"no_exist">>}, User2 = #{zone => external, clientid => <<"no_exist">>, username => <<"test_username">>},
User3 = #{zone => external, clientid => <<"test_clientid">>, username => <<"test_username">>},
allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/test_clientid">>),
deny = emqx_access_control:check_acl(User1, publish, <<"topic/A">>),
deny = emqx_access_control:check_acl(User2, subscribe, <<"topic/test_username">>),
allow = emqx_access_control:check_acl(User2, publish, <<"topic/A">>),
allow = emqx_access_control:check_acl(User3, subscribe, <<"topic/test_clientid">>),
deny = emqx_access_control:check_acl(User3, subscribe, <<"topic/test_username">>),
deny = emqx_access_control:check_acl(User3, publish, <<"topic/A">>),
deny = emqx_access_control:check_acl(User3, subscribe, <<"topic/A/B">>),
deny = emqx_access_control:check_acl(User3, publish, <<"topic/A/B">>),
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"#">>, <<"sub">>, false), ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>),
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"+/A">>, <<"pub">>, false), ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/+">>),
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"Topic/A/B">>, <<"pubsub">>, true), ok = emqx_acl_mnesia_cli:remove_acl({username, <<"test_username">>}, <<"topic/%u">>),
ok = emqx_acl_mnesia_cli:remove_acl({username, <<"test_username">>}, <<"topic/+">>),
ok = emqx_acl_mnesia_cli:remove_acl(all, <<"#">>),
deny = emqx_access_control:check_acl(User1, subscribe, <<"Any">>), ?assertEqual([], emqx_acl_mnesia_cli:all_acls()).
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">>), t_acl_cli(_Config) ->
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:new(emqx_ctl, [non_strict, passthrough]),
meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end), 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, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end),
@ -231,18 +118,67 @@ t_cli(_) ->
meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end), meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end),
clean_all_acls(), 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")), ?assertEqual(0, length(emqx_acl_mnesia_cli:cli(["list"]))),
?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")), emqx_acl_mnesia_cli:cli(["add", "clientid", "test_clientid", "topic/A", "pub", "allow"]),
?assertMatch(["Acl(clientid = <<\"test_clientid\">> topic = <<\"topic/A\">> action = pub access = allow)\n"], emqx_acl_mnesia_cli:cli(["show", "clientid", "test_clientid"])),
?assertMatch(["Acl(clientid = <<\"test_clientid\">> topic = <<\"topic/A\">> action = pub access = allow)\n"], emqx_acl_mnesia_cli:cli(["list", "clientid"])),
emqx_acl_mnesia_cli:cli(["add", "username", "test_username", "topic/B", "sub", "deny"]),
?assertMatch(["Acl(username = <<\"test_username\">> topic = <<\"topic/B\">> action = sub access = deny)\n"], emqx_acl_mnesia_cli:cli(["show", "username", "test_username"])),
?assertMatch(["Acl(username = <<\"test_username\">> topic = <<\"topic/B\">> action = sub access = deny)\n"], emqx_acl_mnesia_cli:cli(["list", "username"])),
emqx_acl_mnesia_cli:cli(["add", "_all", "#", "pubsub", "deny"]),
?assertMatch(["Acl($all topic = <<\"#\">> action = pubsub access = deny)\n"], emqx_acl_mnesia_cli:cli(["list", "_all"])),
?assertEqual(3, length(emqx_acl_mnesia_cli:cli(["list"]))),
emqx_acl_mnesia_cli:cli(["del", "clientid", "test_clientid", "topic/A"]),
emqx_acl_mnesia_cli:cli(["del", "username", "test_username", "topic/B"]),
emqx_acl_mnesia_cli:cli(["del", "_all", "#"]),
?assertEqual(0, length(emqx_acl_mnesia_cli:cli(["list"]))),
meck:unload(emqx_ctl). meck:unload(emqx_ctl).
t_rest_api(_Config) ->
clean_all_acls(),
Params1 = [#{<<"clientid">> => <<"test_clientid">>, <<"topic">> => <<"topic/A">>, <<"action">> => <<"pub">>, <<"access">> => <<"allow">>},
#{<<"clientid">> => <<"test_clientid">>, <<"topic">> => <<"topic/B">>, <<"action">> => <<"sub">>, <<"access">> => <<"allow">>},
#{<<"clientid">> => <<"test_clientid">>, <<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">>}],
{ok, _} = request_http_rest_add([], Params1),
{ok, Re1} = request_http_rest_list(["clientid", "test_clientid"]),
?assertMatch(3, length(get_http_data(Re1))),
{ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/A"]),
{ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/B"]),
{ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/C"]),
{ok, Res1} = request_http_rest_list(["clientid"]),
?assertMatch([], get_http_data(Res1)),
Params2 = [#{<<"username">> => <<"test_username">>, <<"topic">> => <<"topic/A">>, <<"action">> => <<"pub">>, <<"access">> => <<"allow">>},
#{<<"username">> => <<"test_username">>, <<"topic">> => <<"topic/B">>, <<"action">> => <<"sub">>, <<"access">> => <<"allow">>},
#{<<"username">> => <<"test_username">>, <<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">>}],
{ok, _} = request_http_rest_add([], Params2),
{ok, Re2} = request_http_rest_list(["username", "test_username"]),
?assertMatch(3, length(get_http_data(Re2))),
{ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/A"]),
{ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/B"]),
{ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/C"]),
{ok, Res2} = request_http_rest_list(["username"]),
?assertMatch([], get_http_data(Res2)),
Params3 = [#{<<"topic">> => <<"topic/A">>, <<"action">> => <<"pub">>, <<"access">> => <<"allow">>},
#{<<"topic">> => <<"topic/B">>, <<"action">> => <<"sub">>, <<"access">> => <<"allow">>},
#{<<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">>}],
{ok, _} = request_http_rest_add([], Params3),
{ok, Re3} = request_http_rest_list(["$all"]),
?assertMatch(3, length(get_http_data(Re3))),
{ok, _} = request_http_rest_delete(["$all", "topic", "topic/A"]),
{ok, _} = request_http_rest_delete(["$all", "topic", "topic/B"]),
{ok, _} = request_http_rest_delete(["$all", "topic", "topic/C"]),
{ok, Res3} = request_http_rest_list(["$all"]),
?assertMatch([], get_http_data(Res3)).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -255,22 +191,22 @@ clean_all_acls() ->
%% HTTP Request %% HTTP Request
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
request_http_rest_list() -> request_http_rest_list(Path) ->
request_api(get, uri(), default_auth_header()). request_api(get, uri(Path), default_auth_header()).
request_http_rest_lookup(Login) -> request_http_rest_lookup(Path) ->
request_api(get, uri([Login]), default_auth_header()). request_api(get, uri(Path), default_auth_header()).
request_http_rest_add(Params) -> request_http_rest_add(Path, Params) ->
request_api(post, uri(), [], default_auth_header(), Params). request_api(post, uri(Path), [], default_auth_header(), Params).
request_http_rest_delete(Login, Topic) -> request_http_rest_delete(Path) ->
request_api(delete, uri([Login, Topic]), default_auth_header()). request_api(delete, uri(Path), default_auth_header()).
uri() -> uri([]). uri() -> uri([]).
uri(Parts) when is_list(Parts) -> uri(Parts) when is_list(Parts) ->
NParts = [b2l(E) || E <- Parts], NParts = [b2l(E) || E <- Parts],
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "mqtt_acl"| NParts]). ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "acl"| NParts]).
%% @private %% @private
b2l(B) when is_binary(B) -> b2l(B) when is_binary(B) ->

View File

@ -2,10 +2,7 @@
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%% %%
%% Licensed under the Apache License, Version 2.0 (the "License"); %% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with 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
%% 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 %% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS, %% distributed under the License is distributed on an "AS IS" BASIS,
@ -33,6 +30,12 @@
-define(API_VERSION, "v4"). -define(API_VERSION, "v4").
-define(BASE_PATH, "api"). -define(BASE_PATH, "api").
-define(TABLE, emqx_user).
-define(CLIENTID, <<"clientid_for_ct">>).
-define(USERNAME, <<"username_for_ct">>).
-define(PASSWORD, <<"password">>).
-define(NPASSWORD, <<"new_password">>).
all() -> all() ->
emqx_ct:all(?MODULE). emqx_ct:all(?MODULE).
@ -81,143 +84,166 @@ set_special_configs(_App) ->
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_check_as_username(_Config) -> t_management(_Config) ->
clean_all_users(), clean_all_users(),
ok = emqx_auth_mnesia_cli:add_user(<<"test_username">>, <<"password">>, true), ok = emqx_auth_mnesia_cli:add_user({username,?USERNAME}, ?PASSWORD),
{error, existed} = emqx_auth_mnesia_cli:add_user(<<"test_username">>, <<"password">>, true), {error, existed} = emqx_auth_mnesia_cli:add_user({username,?USERNAME}, ?PASSWORD),
?assertMatch([{?TABLE, {username, ?USERNAME}, _Password, _InterTime}], emqx_auth_mnesia_cli:all_users(username)),
ok = emqx_auth_mnesia_cli:update_user(<<"test_username">>, <<"new_password">>, false), ok = emqx_auth_mnesia_cli:add_user({clientid,?CLIENTID}, ?PASSWORD),
{error,noexisted} = emqx_auth_mnesia_cli:update_user(<<"no_existed_user">>, <<"password">>, true), {error, existed} = emqx_auth_mnesia_cli:add_user({clientid,?CLIENTID}, ?PASSWORD),
?assertMatch([{?TABLE, {clientid, ?CLIENTID}, _Password, _InterTime}], emqx_auth_mnesia_cli:all_users(clientid)),
[<<"test_username">>] = emqx_auth_mnesia_cli:all_users(), ?assertEqual(2,length(emqx_auth_mnesia_cli:all_users())),
[{emqx_user, <<"test_username">>, _HashedPass, false}] =
emqx_auth_mnesia_cli:lookup_user(<<"test_username">>),
User1 = #{username => <<"test_username">>, ok = emqx_auth_mnesia_cli:update_user({username,?USERNAME}, ?NPASSWORD),
password => <<"new_password">>, {error,noexisted} = emqx_auth_mnesia_cli:update_user({username, <<"no_existed_user">>}, ?PASSWORD),
ok = emqx_auth_mnesia_cli:update_user({clientid,?CLIENTID}, ?NPASSWORD),
{error,noexisted} = emqx_auth_mnesia_cli:update_user({clientid, <<"no_existed_user">>}, ?PASSWORD),
?assertMatch([{?TABLE, {username, ?USERNAME}, _Password, _InterTime}], emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME})),
?assertMatch([{?TABLE, {clientid, ?CLIENTID}, _Password, _InterTime}], emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID})),
User1 = #{username => ?USERNAME,
clientid => undefined,
password => ?NPASSWORD,
zone => external}, zone => external},
{ok, #{is_superuser := false, {ok, #{auth_result := success,
auth_result := success,
anonymous := false}} = emqx_access_control:authenticate(User1), anonymous := false}} = emqx_access_control:authenticate(User1),
{error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}), {error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}),
ok = emqx_auth_mnesia_cli:remove_user(<<"test_username">>), ok = emqx_auth_mnesia_cli:remove_user({username,?USERNAME}),
{ok, #{auth_result := success, {ok, #{auth_result := success,
anonymous := true }} = emqx_access_control:authenticate(User1). anonymous := true }} = emqx_access_control:authenticate(User1),
t_check_as_clientid(_Config) -> User2 = #{clientid => ?CLIENTID,
clean_all_users(), password => ?NPASSWORD,
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}, 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, {ok, #{auth_result := success,
anonymous := true }} = emqx_access_control:authenticate(User1). anonymous := false}} = emqx_access_control:authenticate(User2),
t_rest_api(_Config) -> {error,password_error} = emqx_access_control:authenticate(User2#{password => <<"error_password">>}),
ok = emqx_auth_mnesia_cli:remove_user({clientid,?CLIENTID}),
{ok, #{auth_result := success,
anonymous := true }} = emqx_access_control:authenticate(User2),
[] = emqx_auth_mnesia_cli:all_users().
t_auth_clientid_cli(_) ->
clean_all_users(), clean_all_users(),
{ok, Result1} = request_http_rest_list(), HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256),
emqx_auth_mnesia_cli:auth_clientid_cli(["add", ?CLIENTID, ?PASSWORD]),
[{_, {clientid, ?CLIENTID}, <<Salt:4/binary, Hash/binary>>, _}] = emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}),
?assertEqual(Hash, emqx_passwd:hash(HashType, <<Salt/binary, ?PASSWORD/binary>>)),
emqx_auth_mnesia_cli:auth_clientid_cli(["update", ?CLIENTID, ?NPASSWORD]),
[{_, {clientid, ?CLIENTID}, <<Salt1:4/binary, Hash1/binary>>, _}] = emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}),
?assertEqual(Hash1, emqx_passwd:hash(HashType, <<Salt1/binary, ?NPASSWORD/binary>>)),
emqx_auth_mnesia_cli:auth_clientid_cli(["del", ?CLIENTID]),
?assertEqual([], emqx_auth_mnesia_cli:lookup_user(?CLIENTID)),
emqx_auth_mnesia_cli:auth_clientid_cli(["add", "user1", "pass1"]),
emqx_auth_mnesia_cli:auth_clientid_cli(["add", "user2", "pass2"]),
?assertEqual(2, length(emqx_auth_mnesia_cli:auth_clientid_cli(["list"]))),
emqx_auth_mnesia_cli:auth_clientid_cli(usage).
t_auth_username_cli(_) ->
clean_all_users(),
HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256),
emqx_auth_mnesia_cli:auth_username_cli(["add", ?USERNAME, ?PASSWORD]),
[{_, {username, ?USERNAME}, <<Salt:4/binary, Hash/binary>>, _}] = emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}),
?assertEqual(Hash, emqx_passwd:hash(HashType, <<Salt/binary, ?PASSWORD/binary>>)),
emqx_auth_mnesia_cli:auth_username_cli(["update", ?USERNAME, ?NPASSWORD]),
[{_, {username, ?USERNAME}, <<Salt1:4/binary, Hash1/binary>>, _}] = emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}),
?assertEqual(Hash1, emqx_passwd:hash(HashType, <<Salt1/binary, ?NPASSWORD/binary>>)),
emqx_auth_mnesia_cli:auth_username_cli(["del", ?USERNAME]),
?assertEqual([], emqx_auth_mnesia_cli:lookup_user(?USERNAME)),
emqx_auth_mnesia_cli:auth_username_cli(["add", "user1", "pass1"]),
emqx_auth_mnesia_cli:auth_username_cli(["add", "user2", "pass2"]),
?assertEqual(2, length(emqx_auth_mnesia_cli:auth_username_cli(["list"]))),
emqx_auth_mnesia_cli:auth_username_cli(usage).
t_clientid_rest_api(_Config) ->
clean_all_users(),
{ok, Result1} = request_http_rest_list(["auth_clientid"]),
[] = get_http_data(Result1), [] = get_http_data(Result1),
Params = #{<<"login">> => <<"test_username">>, <<"password">> => <<"password">>, <<"is_superuser">> => true}, Params1 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD},
{ok, _} = request_http_rest_add(Params), {ok, _} = request_http_rest_add(["auth_clientid"], Params1),
Params1 = [ Params2 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?NPASSWORD},
#{<<"login">> => <<"test_username">>, <<"password">> => <<"password">>, <<"is_superuser">> => true}, {ok, _} = request_http_rest_update(["auth_clientid/" ++ binary_to_list(?CLIENTID)], Params2),
#{<<"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">>), {ok, Result2} = request_http_rest_lookup(["auth_clientid/" ++ binary_to_list(?CLIENTID)]),
#{<<"login">> := <<"test_username">>, <<"is_superuser">> := true} = get_http_data(Result3), ?assertMatch(#{<<"clientid">> := ?CLIENTID}, get_http_data(Result2)),
{ok, _} = request_http_rest_update(<<"test_username">>, <<"new_password">>, error_format), Params3 = [ #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD}
{ok, _} = request_http_rest_update(<<"error_username">>, <<"new_password">>, false), , #{<<"clientid">> => <<"clientid1">>, <<"password">> => ?PASSWORD}
, #{<<"clientid">> => <<"clientid2">>, <<"password">> => ?PASSWORD}
],
{ok, Result3} = request_http_rest_add(["auth_clientid"], Params3),
?assertMatch(#{ ?CLIENTID := <<"{error,existed}">>
, <<"clientid1">> := <<"ok">>
, <<"clientid2">> := <<"ok">>
}, get_http_data(Result3)),
{ok, _} = request_http_rest_update(<<"test_username">>, <<"new_password">>, false), {ok, Result4} = request_http_rest_list(["auth_clientid"]),
{ok, Result4} = request_http_rest_lookup(<<"test_username">>), ?assertEqual(3, length(get_http_data(Result4))),
#{<<"login">> := <<"test_username">>, <<"is_superuser">> := false} = get_http_data(Result4),
User1 = #{username => <<"test_username">>, {ok, _} = request_http_rest_delete(["auth_clientid/" ++ binary_to_list(?CLIENTID)]),
password => <<"new_password">>, {ok, Result5} = request_http_rest_lookup(["auth_clientid/" ++ binary_to_list(?CLIENTID)]),
zone => external}, ?assertMatch(#{}, get_http_data(Result5)).
{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),
t_username_rest_api(_Config) ->
clean_all_users(), clean_all_users(),
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["add", "TestUser", "Password", true]), "ok")), {ok, Result1} = request_http_rest_list(["auth_username"]),
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["add", "TestUser", "Password", true]), "Error")), [] = get_http_data(Result1),
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["update", "NoExisted", "Password", false]), "Error")), Params1 = #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD},
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["update", "TestUser", "Password", false]), "ok")), {ok, _} = request_http_rest_add(["auth_username"], Params1),
?assertMatch(["User(login = <<\"TestUser\">> is_super = false)\n"], emqx_auth_mnesia_cli:auth_cli(["show", "TestUser"])), Params2 = #{<<"username">> => ?USERNAME, <<"password">> => ?NPASSWORD},
?assertMatch(["User(login = <<\"TestUser\">>)\n"], emqx_auth_mnesia_cli:auth_cli(["list"])), {ok, _} = request_http_rest_update(["auth_username/" ++ binary_to_list(?USERNAME)], Params2),
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["del", "TestUser"]), "ok")), {ok, Result2} = request_http_rest_lookup(["auth_username/" ++ binary_to_list(?USERNAME)]),
?assertMatch([], emqx_auth_mnesia_cli:auth_cli(["show", "TestUser"])), ?assertMatch(#{<<"username">> := ?USERNAME}, get_http_data(Result2)),
?assertMatch([], emqx_auth_mnesia_cli:auth_cli(["list"])),
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli([]), "mqtt-user")), Params3 = [ #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD}
, #{<<"username">> => <<"username1">>, <<"password">> => ?PASSWORD}
, #{<<"username">> => <<"username2">>, <<"password">> => ?PASSWORD}
],
{ok, Result3} = request_http_rest_add(["auth_username"], Params3),
?assertMatch(#{ ?USERNAME := <<"{error,existed}">>
, <<"username1">> := <<"ok">>
, <<"username2">> := <<"ok">>
}, get_http_data(Result3)),
meck:unload(emqx_ctl). {ok, Result4} = request_http_rest_list(["auth_username"]),
?assertEqual(3, length(get_http_data(Result4))),
{ok, _} = request_http_rest_delete(["auth_username/" ++ binary_to_list(?USERNAME)]),
{ok, Result5} = request_http_rest_lookup(["auth_username/" ++ binary_to_list(?USERNAME)]),
?assertMatch(#{}, get_http_data(Result5)).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -231,18 +257,17 @@ clean_all_users() ->
%% HTTP Request %% HTTP Request
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
request_http_rest_list() -> request_http_rest_list(Path) ->
request_api(get, uri(), default_auth_header()). request_api(get, uri(Path), default_auth_header()).
request_http_rest_lookup(Login) -> request_http_rest_lookup(Path) ->
request_api(get, uri([Login]), default_auth_header()). request_api(get, uri([Path]), default_auth_header()).
request_http_rest_add(Params) -> request_http_rest_add(Path, Params) ->
request_api(post, uri(), [], default_auth_header(), Params). request_api(post, uri(Path), [], default_auth_header(), Params).
request_http_rest_update(Login, Password, IsSuperuser) -> request_http_rest_update(Path, Params) ->
Params = #{<<"password">> => Password, <<"is_superuser">> => IsSuperuser}, request_api(put, uri([Path]), [], default_auth_header(), Params).
request_api(put, uri([Login]), [], default_auth_header(), Params).
request_http_rest_delete(Login) -> request_http_rest_delete(Login) ->
request_api(delete, uri([Login]), default_auth_header()). request_api(delete, uri([Login]), default_auth_header()).
@ -250,7 +275,7 @@ request_http_rest_delete(Login) ->
uri() -> uri([]). uri() -> uri([]).
uri(Parts) when is_list(Parts) -> uri(Parts) when is_list(Parts) ->
NParts = [b2l(E) || E <- Parts], NParts = [b2l(E) || E <- Parts],
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "mqtt_user"| NParts]). ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
%% @private %% @private
b2l(B) when is_binary(B) -> b2l(B) when is_binary(B) ->

View File

@ -1,111 +0,0 @@
emqx_auth_username
==================
EMQ X Authentication with Username and Password
Build
-----
```
make && make tests
```
Configuration
-------------
etc/emqx_auth_username.conf:
```
## Password hash.
##
## Value: plain | md5 | sha | sha256
auth.user.password_hash = sha256
```
[REST API](https://developer.emqx.io/docs/emq/v3/en/rest.html)
------------
List all usernames
```
# Request
GET api/v4/auth_username
# Response
{
"code": 0,
"data": ["username1"]
}
```
Add a username:
```
# Request
POST api/v4/auth_username
{
"username": "some_name",
"password": "password"
}
# Response
{
"code": 0
}
```
Update password for a username:
```
# Request
PUT api/v4/auth_username/$NAME
{
"password": "password"
}
# Response
{
"code", 0
}
```
Lookup a username info:
```
# Request
GET api/v4/auth_username/$NAME
# Response
{
"code": 0,
"data": {
"username": "some_username",
"password": "hashed_password"
}
}
```
Delete a username:
```
# Request
DELETE api/v4/auth_username/$NAME
# Response
{
"code": 0
}
```
Load the Plugin
---------------
```
./bin/emqx_ctl plugins load emqx_auth_username
```
License
-------
Apache License Version 2.0
Author
------
EMQ X Team.

View File

@ -1 +0,0 @@
Upgrade test cases for 3.0

View File

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

View File

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

View File

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

View File

@ -1,14 +0,0 @@
{application, emqx_auth_username,
[{description, "EMQ X Authentication with Username and Password"},
{vsn, "5.0.0"}, % strict semver, bump manually!
{modules, []},
{registered, []},
{applications, [kernel,stdlib,emqx_libs]},
{mod, {emqx_auth_username_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-username"}
]}
]}.

View File

@ -1,173 +0,0 @@
%%--------------------------------------------------------------------
%% 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_username).
-include("emqx_auth_username.hrl").
-include_lib("emqx_libs/include/emqx.hrl").
%% CLI callbacks
-export([cli/1]).
%% APIs
-export([ add_user/2
, update_password/2
, remove_user/1
, lookup_user/1
, all_users/0
]).
-export([unwrap_salt/1]).
%% Auth callbacks
-export([ init/1
, register_metrics/0
, check/3
, description/0
]).
-define(TAB, ?MODULE).
-record(?TAB, {username, password}).
%%--------------------------------------------------------------------
%% CLI
%%--------------------------------------------------------------------
cli(["list"]) ->
Usernames = mnesia:dirty_all_keys(?TAB),
[emqx_ctl:print("~s~n", [Username]) || Username <- Usernames];
cli(["add", Username, Password]) ->
Ok = add_user(iolist_to_binary(Username), iolist_to_binary(Password)),
emqx_ctl:print("~p~n", [Ok]);
cli(["update", Username, NewPassword]) ->
Ok = update_password(iolist_to_binary(Username), iolist_to_binary(NewPassword)),
emqx_ctl:print("~p~n", [Ok]);
cli(["del", Username]) ->
emqx_ctl:print("~p~n", [remove_user(iolist_to_binary(Username))]);
cli(_) ->
emqx_ctl:usage([{"users list", "List users"},
{"users add <Username> <Password>", "Add User"},
{"users update <Username> <NewPassword>", "Update User"},
{"users del <Username>", "Delete User"}]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Add User
-spec(add_user(binary(), binary()) -> ok | {error, any()}).
add_user(Username, Password) ->
User = #?TAB{username = Username, password = encrypted_data(Password)},
ret(mnesia:transaction(fun insert_user/1, [User])).
insert_user(User = #?TAB{username = Username}) ->
case mnesia:read(?TAB, Username) of
[] -> mnesia:write(User);
[_|_] -> mnesia:abort(existed)
end.
%% @doc Update User
-spec(update_password(binary(), binary()) -> ok | {error, any()}).
update_password(Username, NewPassword) ->
User = #?TAB{username = Username, password = encrypted_data(NewPassword)},
ret(mnesia:transaction(fun do_update_password/1, [User])).
do_update_password(User = #?TAB{username = Username}) ->
case mnesia:read(?TAB, Username) of
[_|_] -> mnesia:write(User);
[] -> mnesia:abort(noexisted)
end.
%% @doc Lookup user by username
-spec(lookup_user(binary()) -> list()).
lookup_user(Username) ->
mnesia:dirty_read(?TAB, Username).
%% @doc Remove user
-spec(remove_user(binary()) -> ok | {error, any()}).
remove_user(Username) ->
ret(mnesia:transaction(fun mnesia:delete/1, [{?TAB, Username}])).
ret({atomic, ok}) -> ok;
ret({aborted, Error}) -> {error, Error}.
%% @doc All usernames
-spec(all_users() -> list()).
all_users() -> mnesia:dirty_all_keys(?TAB).
unwrap_salt(<<_Salt:4/binary, HashPasswd/binary>>) ->
HashPasswd.
%%--------------------------------------------------------------------
%% Auth callbacks
%%--------------------------------------------------------------------
init(DefaultUsers) ->
ok = ekka_mnesia:create_table(?TAB, [
{disc_copies, [node()]},
{attributes, record_info(fields, ?TAB)},
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
ok = lists:foreach(fun add_default_user/1, DefaultUsers),
ok = ekka_mnesia:copy_table(?TAB, disc_copies).
%% @private
add_default_user({Username, Password}) ->
add_user(iolist_to_binary(Username), iolist_to_binary(Password)).
-spec(register_metrics() -> ok).
register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
check(#{username := Username, password := Password}, AuthResult, #{hash_type := HashType}) ->
case mnesia:dirty_read(?TAB, Username) of
[] -> emqx_metrics:inc(?AUTH_METRICS(ignore));
[#?TAB{password = <<Salt:4/binary, Hash/binary>>}] ->
case Hash =:= hash(Password, Salt, HashType) of
true ->
ok = emqx_metrics:inc(?AUTH_METRICS(success)),
{stop, AuthResult#{auth_result => success, anonymous => false}};
false ->
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
{stop, AuthResult#{auth_result => not_authorized, anonymous => false}}
end
end.
description() ->
"Username password Authentication Module".
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
encrypted_data(Password) ->
HashType = application:get_env(emqx_auth_username, 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>>.

View File

@ -1,123 +0,0 @@
%%--------------------------------------------------------------------
%% 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_username_api).
-include("emqx_auth_username.hrl").
-import(proplists, [get_value/2]).
-import(minirest, [return/0, return/1]).
-rest_api(#{name => list_username,
method => 'GET',
path => "/auth_username",
func => list,
descr => "List available username in the cluster"
}).
-rest_api(#{name => lookup_username,
method => 'GET',
path => "/auth_username/:bin:username",
func => lookup,
descr => "Lookup username in the cluster"
}).
-rest_api(#{name => add_username,
method => 'POST',
path => "/auth_username",
func => add,
descr => "Add username in the cluster"
}).
-rest_api(#{name => update_username,
method => 'PUT',
path => "/auth_username/:bin:username",
func => update,
descr => "Update username in the cluster"
}).
-rest_api(#{name => delete_username,
method => 'DELETE',
path => "/auth_username/:bin:username",
func => delete,
descr => "Delete username in the cluster"
}).
-export([ list/2
, lookup/2
, add/2
, update/2
, delete/2
]).
list(_Bindings, _Params) ->
return({ok, emqx_auth_username:all_users()}).
lookup(#{username := Username}, _Params) ->
return({ok, format(emqx_auth_username:lookup_user(Username))}).
add(_Bindings, Params) ->
Username = get_value(<<"username">>, Params),
Password = get_value(<<"password">>, Params),
case validate([username, password], [Username, Password]) of
ok ->
case emqx_auth_username:add_user(Username, Password) of
ok -> return();
Err -> return(Err)
end;
Err -> return(Err)
end.
update(#{username := Username}, Params) ->
Password = get_value(<<"password">>, Params),
case validate([password], [Password]) of
ok ->
case emqx_auth_username:update_password(Username, Password) of
ok -> return();
Err -> return(Err)
end;
Err -> return(Err)
end.
delete(#{username := Username}, _) ->
ok = emqx_auth_username:remove_user(Username),
return().
%%------------------------------------------------------------------------------
%% Interval Funcs
%%------------------------------------------------------------------------------
format([{?APP, Username, Password}]) ->
#{username => Username,
password => emqx_auth_username:unwrap_salt(Password)}.
validate([], []) ->
ok;
validate([K|Keys], [V|Values]) ->
case validation(K, V) of
false -> {error, K};
true -> validate(Keys, Values)
end.
validation(username, 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.

View File

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

View File

@ -1,176 +0,0 @@
%% 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_username_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(TAB, emqx_auth_username).
all() ->
emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([emqx_auth_username], fun set_special_configs/1),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([emqx_auth_username]).
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(_Config) ->
clean_all_users(),
ok = emqx_auth_username:add_user(<<"test_username">>, <<"password">>),
[{?TAB, <<"test_username">>, _HashedPass}] =
emqx_auth_username:lookup_user(<<"test_username">>),
User1 = #{username => <<"test_username">>,
password => <<"password">>,
zone => external},
{ok, #{auth_result := success,
anonymous := false}} = emqx_access_control:authenticate(User1),
ok = emqx_auth_username:remove_user(<<"test_username">>),
{ok, #{auth_result := success,
anonymous := true }} = emqx_access_control:authenticate(User1).
t_rest_api(_Config) ->
clean_all_users(),
Username = <<"username">>,
Password = <<"password">>,
Password1 = <<"password1">>,
User = #{username => Username, zone => external},
?assertEqual(return(),
emqx_auth_username_api:add(#{}, rest_params(Username, Password))),
?assertEqual(return({error, existed}),
emqx_auth_username_api:add(#{}, rest_params(Username, Password))),
?assertEqual(return([Username]),
emqx_auth_username_api:list(#{}, [])),
{ok, #{code := 0, data := Data}} =
emqx_auth_username_api:lookup(rest_binding(Username), []),
?assertEqual(true, match_password(maps:get(username, Data), Password)),
{ok, _} = emqx_access_control:authenticate(User#{password => Password}),
?assertEqual(return(),
emqx_auth_username_api:update(rest_binding(Username), rest_params(Password))),
?assertEqual(return({error, noexisted}),
emqx_auth_username_api:update(#{username => <<"another_user">>}, rest_params(<<"another_passwd">>))),
{error, _} = emqx_access_control:authenticate(User#{password => Password1}),
?assertEqual(return(),
emqx_auth_username_api:delete(rest_binding(Username), [])),
{ok, #{auth_result := success,
anonymous := true}} = emqx_access_control:authenticate(User#{password => Password}).
t_cli(_Config) ->
clean_all_users(),
emqx_auth_username:cli(["add", "username", "password"]),
?assertEqual(true, match_password(<<"username">>, <<"password">>)),
emqx_auth_username:cli(["update", "username", "newpassword"]),
?assertEqual(true, match_password(<<"username">>, <<"newpassword">>)),
emqx_auth_username:cli(["del", "username"]),
[] = emqx_auth_username:lookup_user(<<"username">>),
emqx_auth_username:cli(["add", "user1", "pass1"]),
emqx_auth_username:cli(["add", "user2", "pass2"]),
UserList = emqx_auth_username:cli(["list"]),
2 = length(UserList),
emqx_auth_username:cli(usage).
t_conf_not_override_existed(_) ->
clean_all_users(),
Username = <<"username">>,
Password = <<"password">>,
NPassword = <<"password1">>,
User = #{username => Username, zone => external},
application:stop(emqx_auth_username),
application:set_env(emqx_auth_username, userlist, [{Username, Password}]),
application:ensure_all_started(emqx_auth_username),
{ok, _} = emqx_access_control:authenticate(User#{password => Password}),
emqx_auth_username:cli(["update", Username, NPassword]),
{error, _} = emqx_access_control:authenticate(User#{password => Password}),
{ok, _} = emqx_access_control:authenticate(User#{password => NPassword}),
application:stop(emqx_auth_username),
application:ensure_all_started(emqx_auth_username),
{ok, _} = emqx_access_control:authenticate(User#{password => NPassword}),
?assertEqual(return(),
emqx_auth_username_api:update(rest_binding(Username), rest_params(Password))),
application:stop(emqx_auth_username),
application:ensure_all_started(emqx_auth_username),
{ok, _} = emqx_access_control:authenticate(User#{password => Password}).
%%------------------------------------------------------------------------------
%% Helpers
%%------------------------------------------------------------------------------
clean_all_users() ->
[ mnesia:dirty_delete({emqx_auth_username, Username})
|| Username <- mnesia:dirty_all_keys(emqx_auth_username)].
match_password(Username, PlainPassword) ->
HashType = application:get_env(emqx_auth_username, password_hash, sha256),
[{?TAB, Username, <<Salt:4/binary, Hash/binary>>}] =
emqx_auth_username:lookup_user(Username),
Hash =:= emqx_passwd:hash(HashType, <<Salt/binary, PlainPassword/binary>>).
rest_params(Passwd) ->
[{<<"password">>, Passwd}].
rest_params(Username, Passwd) ->
[{<<"username">>, Username},
{<<"password">>, Passwd}].
rest_binding(Username) ->
#{username => Username}.
return() ->
{ok, #{code => 0}}.
return({error, Err}) ->
{ok, #{message => Err}};
return(Data) ->
{ok, #{code => 0, data => Data}}.

View File

@ -119,8 +119,6 @@
, {emqx_sn, load} , {emqx_sn, load}
, {emqx_coap, load} , {emqx_coap, load}
, {emqx_stomp, load} , {emqx_stomp, load}
, {emqx_auth_clientid, load}
, {emqx_auth_username, load}
, {emqx_auth_http, load} , {emqx_auth_http, load}
, {emqx_auth_mysql, load} , {emqx_auth_mysql, load}
, {emqx_auth_jwt, load} , {emqx_auth_jwt, load}