diff --git a/apps/emqx_auth_clientid/README.md b/apps/emqx_auth_clientid/README.md deleted file mode 100644 index 991e74cb1..000000000 --- a/apps/emqx_auth_clientid/README.md +++ /dev/null @@ -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. - diff --git a/apps/emqx_auth_clientid/TODO b/apps/emqx_auth_clientid/TODO deleted file mode 100644 index a6e013c83..000000000 --- a/apps/emqx_auth_clientid/TODO +++ /dev/null @@ -1,3 +0,0 @@ -1. Hash password -2. Add Test cases -3. Hot Reloader diff --git a/apps/emqx_auth_clientid/include/emqx_auth_clientid.hrl b/apps/emqx_auth_clientid/include/emqx_auth_clientid.hrl deleted file mode 100644 index 328a46bcd..000000000 --- a/apps/emqx_auth_clientid/include/emqx_auth_clientid.hrl +++ /dev/null @@ -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)). \ No newline at end of file diff --git a/apps/emqx_auth_clientid/priv/emqx_auth_clientid.schema b/apps/emqx_auth_clientid/priv/emqx_auth_clientid.schema deleted file mode 100644 index db21d8e3e..000000000 --- a/apps/emqx_auth_clientid/priv/emqx_auth_clientid.schema +++ /dev/null @@ -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}. - diff --git a/apps/emqx_auth_clientid/rebar.config b/apps/emqx_auth_clientid/rebar.config deleted file mode 100644 index 7b1f06cca..000000000 --- a/apps/emqx_auth_clientid/rebar.config +++ /dev/null @@ -1 +0,0 @@ -{deps, []}. \ No newline at end of file diff --git a/apps/emqx_auth_clientid/src/emqx_auth_clientid.app.src b/apps/emqx_auth_clientid/src/emqx_auth_clientid.app.src deleted file mode 100644 index cf96e6eca..000000000 --- a/apps/emqx_auth_clientid/src/emqx_auth_clientid.app.src +++ /dev/null @@ -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 "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-clientid"} - ]} - ]}. diff --git a/apps/emqx_auth_clientid/src/emqx_auth_clientid.erl b/apps/emqx_auth_clientid/src/emqx_auth_clientid.erl deleted file mode 100644 index 6643971ed..000000000 --- a/apps/emqx_auth_clientid/src/emqx_auth_clientid.erl +++ /dev/null @@ -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 ", "Add ClientId"}, - {"clientid update ", "Update Clientid"}, - {"clientid del ", "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 = <>}] -> - 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(), - <>. - -hash(undefined, SaltBin, HashType) -> - hash(<<>>, SaltBin, HashType); -hash(Password, SaltBin, HashType) -> - emqx_passwd:hash(HashType, <>). - -salt() -> - rand:seed(exsplus, erlang:timestamp()), - Salt = rand:uniform(16#ffffffff), <>. - diff --git a/apps/emqx_auth_clientid/src/emqx_auth_clientid_api.erl b/apps/emqx_auth_clientid/src/emqx_auth_clientid_api.erl deleted file mode 100644 index 6da5b14e8..000000000 --- a/apps/emqx_auth_clientid/src/emqx_auth_clientid_api.erl +++ /dev/null @@ -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. diff --git a/apps/emqx_auth_clientid/src/emqx_auth_clientid_app.erl b/apps/emqx_auth_clientid/src/emqx_auth_clientid_app.erl deleted file mode 100644 index 4c7b242b4..000000000 --- a/apps/emqx_auth_clientid/src/emqx_auth_clientid_app.erl +++ /dev/null @@ -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}, []} }. - diff --git a/apps/emqx_auth_clientid/test/emqx_auth_clientid_SUITE.erl b/apps/emqx_auth_clientid/test/emqx_auth_clientid_SUITE.erl deleted file mode 100644 index 24ab0f7ce..000000000 --- a/apps/emqx_auth_clientid/test/emqx_auth_clientid_SUITE.erl +++ /dev/null @@ -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, <>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID), - ?assertEqual(Hash, emqx_passwd:hash(HashType, <>)), - - emqx_auth_clientid:cli(["update", ?CLIENTID, ?NPASSWORD]), - [{_, ?CLIENTID, <>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID), - ?assertEqual(Hash1, emqx_passwd:hash(HashType, <>)), - - 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, <>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID), - ?assertEqual(Hash, emqx_passwd:hash(HashType, <>)), - - {ok, _} = request_http_rest_update(?CLIENTID, ?NPASSWORD), - [{_, ?CLIENTID, <>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID), - ?assertEqual(Hash1, emqx_passwd:hash(HashType, <>)), - - {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/// - ?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. - diff --git a/apps/emqx_auth_mnesia/etc/emqx_auth_mnesia.conf b/apps/emqx_auth_mnesia/etc/emqx_auth_mnesia.conf new file mode 100644 index 000000000..ff74656cb --- /dev/null +++ b/apps/emqx_auth_mnesia/etc/emqx_auth_mnesia.conf @@ -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~!@#$%^&*()_+ diff --git a/apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl b/apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl index 31557ea63..034bd4f30 100644 --- a/apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl +++ b/apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl @@ -1,17 +1,20 @@ -define(APP, emqx_auth_mnesia). +-type(login():: {clientid, binary()} + | {username, binary()}). + -record(emqx_user, { - login, - password, - is_superuser - }). + login :: login(), + password :: binary(), + created_at :: integer() + }). -record(emqx_acl, { - login, - topic, - action, - allow - }). + filter:: {login() | all, emqx_topic:topic()}, + action :: pub | sub | pubsub, + access :: allow | deny, + created_at :: integer() + }). -record(auth_metrics, { success = 'client.auth.success', @@ -32,4 +35,4 @@ -define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). -define(ACL_METRICS, ?METRICS(acl_metrics)). --define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). \ No newline at end of file +-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)). diff --git a/apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema b/apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema index 34333b145..87d6bf47f 100644 --- a/apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema +++ b/apps/emqx_auth_mnesia/priv/emqx_auth_mnesia.schema @@ -1,34 +1,42 @@ %%-*- mode: erlang -*- %% emqx_auth_mnesia config mapping -{mapping, "auth.mnesia.as", "emqx_auth_mnesia.as", [ - {default, username}, - {datatype, {enum, [username, clientid]}} -]}. - {mapping, "auth.mnesia.password_hash", "emqx_auth_mnesia.password_hash", [ {default, sha256}, - {datatype, {enum, [plain, md5, sha, sha256]}} + {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} ]}. -{mapping, "auth.mnesia.$id.password", "emqx_auth_mnesia.userlist", [ +{mapping, "auth.client.$id.password", "emqx_auth_mnesia.clientid_list", [ {datatype, string} ]}. -{mapping, "auth.mnesia.$id.is_superuser", "emqx_auth_mnesia.userlist", [ - {default, false}, - {datatype, {enum, [false, true]}} -]}. - -{translation, "emqx_auth_mnesia.userlist", fun(Conf) -> - Userlist = cuttlefish_variable:filter_by_prefix("auth.mnesia", Conf), +{translation, "emqx_auth_mnesia.clientid_list", fun(Conf) -> + ClientList = cuttlefish_variable:filter_by_prefix("auth.client", Conf), lists:foldl( - fun({["auth", "mnesia", Id, "login"], Username}, AccIn) -> - [{Username, cuttlefish:conf_get("auth.mnesia." ++ Id ++ ".password", Conf), cuttlefish:conf_get("auth.mnesia." ++ Id ++ ".is_superuser", Conf)} | AccIn]; + fun({["auth", "client", Id, "clientid"], ClientId}, 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 end, [], Userlist) diff --git a/apps/emqx_auth_mnesia/rebar.config b/apps/emqx_auth_mnesia/rebar.config index 7b1f06cca..684837fb5 100644 --- a/apps/emqx_auth_mnesia/rebar.config +++ b/apps/emqx_auth_mnesia/rebar.config @@ -1 +1,29 @@ -{deps, []}. \ No newline at end of file +{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}. + diff --git a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl index 63a357979..c657e54a0 100644 --- a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl +++ b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl @@ -18,6 +18,10 @@ -include("emqx_auth_mnesia.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-define(TABLE, emqx_acl). + %% ACL Callbacks -export([ init/0 , register_metrics/0 @@ -27,22 +31,38 @@ init() -> ok = ekka_mnesia:create_table(emqx_acl, [ - {type, bag}, {disc_copies, [node()]}, {attributes, record_info(fields, emqx_acl)}, {storage_properties, [{ets, [{read_concurrency, true}]}]}]), - ok = ekka_mnesia:copy_table(emqx_user, disc_copies). + ok = ekka_mnesia:copy_table(emqx_acl, disc_copies). -spec(register_metrics() -> ok). register_metrics() -> lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS). -check_acl(ClientInfo, PubSub, Topic, NoMatchAction, #{key_as := As}) -> - Login = maps:get(As, ClientInfo), - case do_check_acl(Login, PubSub, Topic, NoMatchAction) of - ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok; - {stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow}; - {stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny} +check_acl(ClientInfo = #{ clientid := Clientid }, PubSub, Topic, _NoMatchAction, _Params) -> + Username = maps:get(username, ClientInfo, undefined), + + Acls = case Username of + undefined -> + 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. description() -> "Acl with Mnesia". @@ -51,33 +71,33 @@ description() -> "Acl with Mnesia". %% Internal functions %%------------------------------------------------------------------- -do_check_acl(Login, PubSub, Topic, _NoMatchAction) -> - case match(PubSub, Topic, emqx_auth_mnesia_cli:lookup_acl(Login)) of - allow -> {stop, allow}; - deny -> {stop, deny}; - _ -> - case match(PubSub, Topic, emqx_auth_mnesia_cli:lookup_acl(<<"$all">>)) of - allow -> {stop, allow}; - deny -> {stop, deny}; - _ -> ok - end - end. - -match(_PubSub, _Topic, []) -> +match(_ClientInfo, _PubSub, _Topic, []) -> nomatch; -match(PubSub, Topic, [ #emqx_acl{topic = ACLTopic, action = Action, allow = Allow} | UserAcl]) -> - case match_actions(PubSub, Action) andalso match_topic(Topic, ACLTopic) of - true -> case Allow of - true -> allow; - _ -> deny - end; - false -> match(PubSub, Topic, UserAcl) +match(ClientInfo, PubSub, Topic, [ {_, ACLTopic, Action, Access, _} | Acls]) -> + case match_actions(PubSub, Action) andalso match_topic(ClientInfo, Topic, ACLTopic) of + true -> Access; + false -> match(ClientInfo, PubSub, Topic, Acls) end. -match_topic(Topic, ACLTopic) when is_binary(Topic) -> - emqx_topic:match(Topic, ACLTopic). +match_topic(ClientInfo, Topic, ACLTopic) when is_binary(Topic) -> + emqx_topic:match(Topic, feed_var(ClientInfo, ACLTopic)). -match_actions(_, <<"pubsub">>) -> true; -match_actions(subscribe, <<"sub">>) -> true; -match_actions(publish, <<"pub">>) -> true; +match_actions(_, pubsub) -> true; +match_actions(subscribe, sub) -> true; +match_actions(publish, pub) -> true; 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]). diff --git a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl index 3891b7ffd..8f858dfa6 100644 --- a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl +++ b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl @@ -1,4 +1,4 @@ -%%-------------------------------------------------------------------- +%c%-------------------------------------------------------------------- %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,94 +18,174 @@ -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]). --rest_api(#{name => list_emqx_acl, +-rest_api(#{name => list_clientid, method => 'GET', - path => "/mqtt_acl", - func => list, + path => "/acl/clientid", + func => list_clientid, descr => "List available mnesia in the cluster" }). --rest_api(#{name => lookup_emqx_acl, +-rest_api(#{name => list_username, 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, 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', - path => "/mqtt_acl", + path => "/acl", func => add, descr => "Add mnesia in the cluster" }). --rest_api(#{name => delete_emqx_acl, +-rest_api(#{name => delete_clientid, method => 'DELETE', - path => "/mqtt_acl/:bin:login/:bin:topic", + path => "/acl/clientid/:bin:clientid/topic/:bin:topic", func => delete, 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 , add/2 , delete/2 ]). -list(_Bindings, Params) -> - return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, Params, fun format/1)}). +list_clientid(_Bindings, Params) -> + 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) -> - return({ok, format(emqx_auth_mnesia_cli:lookup_acl(urldecode(Login)))}). +list_username(_Bindings, Params) -> + 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) -> [ P | _] = Params, case is_list(P) of - true -> return(add_acl(Params, [])); - false -> return(add_acl([Params], [])) + true -> return(do_add(Params, [])); + false -> + Re = do_add(Params), + case Re of + #{result := ok} -> return({ok, Re}); + #{result := <<"ok">>} -> return({ok, Re}); + _ -> return({error, Re}) + end end. -add_acl([ Params | ParamsN ], ReList ) -> - Login = urldecode(get_value(<<"login">>, Params)), - Topic = urldecode(get_value(<<"topic">>, Params)), - Action = urldecode(get_value(<<"action">>, Params)), - Allow = get_value(<<"allow">>, Params), - Re = case validate([login, topic, action, allow], [Login, Topic, Action, Allow]) of - ok -> - emqx_auth_mnesia_cli:add_acl(Login, Topic, Action, Allow); - Err -> Err - end, - add_acl(ParamsN, [{Login, format_msg(Re)} | ReList]); - -add_acl([], ReList) -> +do_add([ Params | ParamsN ], ReList) -> + do_add(ParamsN, [do_add(Params) | ReList]); + +do_add([], ReList) -> {ok, ReList}. -delete(#{login := Login, topic := Topic}, _) -> - return(emqx_auth_mnesia_cli:remove_acl(urldecode(Login), urldecode(Topic))). +do_add(Params) -> + 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 %%------------------------------------------------------------------------------ - -format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) -> - #{login => Login, topic => Topic, action => Action, allow => Allow }; - -format([]) -> - #{}; - -format([#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}]) -> - format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}); - -format([ #emqx_acl{login = _Key, topic = _Topic, action = _Action, allow = _Allow}| _] = List) -> +format({{clientid, Clientid}, Topic, Action, Access, _CreatedAt}) -> + #{clientid => Clientid, topic => Topic, action => Action, access => Access}; +format({{username, Username}, Topic, Action, Access, _CreatedAt}) -> + #{username => Username, topic => Topic, action => Action, access => Access}; +format({all, Topic, Action, Access, _CreatedAt}) -> + #{all => '$all', topic => Topic, action => Action, access => Access}; +format(List) when is_list(List) -> format(List, []). - -format([#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow} | List], ReList) -> - format(List, [ format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) | ReList]); -format([], ReList) -> ReList. + +format([L | List], Relist) -> + format(List, [format(L) | Relist]); +format([], ReList) -> lists:reverse(ReList). validate([], []) -> ok; @@ -114,8 +194,18 @@ validate([K|Keys], [V|Values]) -> false -> {error, K}; true -> validate(Keys, Values) end. - -do_validation(login, V) when is_binary(V) +do_validation(login, all) -> + 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 -> true; do_validation(topic, V) when is_binary(V) @@ -126,7 +216,7 @@ do_validation(action, V) when is_binary(V) -> true -> true; false -> false end; -do_validation(allow, V) when is_boolean(V) -> +do_validation(access, V) when V =:= <<"allow">> orelse V =:= <<"deny">> -> true; do_validation(_, _) -> false. @@ -145,4 +235,3 @@ urldecode(S) -> urldecode(S) -> http_uri:decode(S). -endif. - diff --git a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl new file mode 100644 index 000000000..1eb3e752d --- /dev/null +++ b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl @@ -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 ", "Lookup clientid acl detail"} + , {"acl show username ", "Lookup username acl detail"} + , {"acl aad clientid ", "Add clientid acl"} + , {"acl add Username ", "Add username acl"} + , {"acl add _all ", "Add $all acl"} + , {"acl del clientid ", "Delete clientid acl"} + , {"acl del username ", "Delete username acl"} + , {"acl del _all, ", "Delete $all acl"} + ]). + + diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src index c1e4aec18..f51395f2c 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src @@ -1,9 +1,9 @@ {application, emqx_auth_mnesia, [{description, "EMQ X Authentication with Mnesia"}, - {vsn, "5.0.0"}, % strict semver, bump manually! + {vsn, "4.3.0"}, % strict semver, bump manually {modules, []}, {registered, []}, - {applications, [kernel,stdlib,mnesia,emqx_libs]}, + {applications, [kernel,stdlib,mnesia]}, {mod, {emqx_auth_mnesia_app,[]}}, {env, []}, {licenses, ["Apache-2.0"]}, diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl index 8b47c8b32..f0e78ae36 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.erl @@ -22,6 +22,9 @@ -include_lib("emqx_libs/include/logger.hrl"). -include_lib("emqx_libs/include/types.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-define(TABLE, emqx_user). %% Auth callbacks -export([ init/1 , register_metrics/0 @@ -29,48 +32,53 @@ , description/0 ]). -init(DefaultUsers) -> +init(#{clientid_list := ClientidList, username_list := UsernameList}) -> ok = ekka_mnesia:create_table(emqx_user, [ {disc_copies, [node()]}, {attributes, record_info(fields, emqx_user)}, {storage_properties, [{ets, [{read_concurrency, true}]}]}]), - ok = lists:foreach(fun add_default_user/1, DefaultUsers), + [ 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). %% @private -add_default_user({Login, Password, IsSuperuser}) -> - emqx_auth_mnesia_cli:add_user(iolist_to_binary(Login), iolist_to_binary(Password), IsSuperuser). +add_default_user({Login, Password}) when is_tuple(Login) -> + emqx_auth_mnesia_cli:add_user(Login, Password). -spec(register_metrics() -> ok). register_metrics() -> lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). -check(ClientInfo = #{password := Password}, AuthResult, #{hash_type := HashType, key_as := As}) -> - Login = maps:get(As, ClientInfo), - case emqx_auth_mnesia_cli:lookup_user(Login) of +check(ClientInfo = #{ clientid := Clientid + , password := NPassword + }, 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)), ok; - [User] -> - case emqx_passwd:check_pass({User#emqx_user.password, Password}, HashType) of - ok -> - emqx_metrics:inc(?AUTH_METRICS(success)), - {stop, AuthResult#{is_superuser => is_superuser(User), - anonymous => false, - auth_result => success}}; - {error, Reason} -> - ?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [Reason]), + List -> + case [ Hash || <> <- lists:sort(fun emqx_auth_mnesia_cli:comparing/2, List), + Hash =:= hash(NPassword, Salt, HashType) + ] of + [] -> + ?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [ClientInfo]), 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. description() -> "Authentication with Mnesia". -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- -is_superuser(#emqx_user{is_superuser = true}) -> - true; -is_superuser(_) -> - false. +hash(undefined, SaltBin, HashType) -> + hash(<<>>, SaltBin, HashType); +hash(Password, SaltBin, HashType) -> + emqx_passwd:hash(HashType, <>). diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl index 7c527b0fd..ee52fcf36 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl @@ -17,100 +17,206 @@ -module(emqx_auth_mnesia_api). -include_lib("stdlib/include/qlc.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +-define(TABLE, emqx_user). -import(proplists, [get_value/2]). - -import(minirest, [return/1]). +-export([paginate/5]). --rest_api(#{name => list_emqx_user, - method => 'GET', - path => "/mqtt_user", - func => list, - descr => "List available mnesia in the cluster" - }). - --rest_api(#{name => lookup_emqx_user, - method => 'GET', - path => "/mqtt_user/:bin:login", - func => lookup, - descr => "Lookup mnesia in the cluster" - }). - --rest_api(#{name => add_emqx_user, - method => 'POST', - path => "/mqtt_user", - func => add, - descr => "Add mnesia in the cluster" - }). - --rest_api(#{name => update_emqx_user, - method => 'PUT', - path => "/mqtt_user/:bin:login", - func => update, - descr => "Update mnesia in the cluster" - }). - --rest_api(#{name => delete_emqx_user, - method => 'DELETE', - path => "/mqtt_user/:bin:login", - func => delete, - descr => "Delete mnesia in the cluster" - }). - --export([ list/2 - , lookup/2 - , add/2 - , update/2 - , delete/2 +-export([ list_clientid/2 + , lookup_clientid/2 + , add_clientid/2 + , update_clientid/2 + , delete_clientid/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) -> - return({ok, paginate(emqx_user, Params, fun format/1)}). +-rest_api(#{name => lookup_clientid, + method => 'GET', + path => "/auth_clientid/:bin:clientid", + func => lookup_clientid, + descr => "Lookup clientid in the cluster" + }). -lookup(#{login := Login}, _Params) -> - return({ok, format(emqx_auth_mnesia_cli:lookup_user(urldecode(Login)))}). +-rest_api(#{name => add_clientid, + 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, case is_list(P) of - true -> return(add_user(Params, [])); - false -> return(add_user([Params], [])) + true -> return(do_add_clientid(Params, [])); + false -> + Re = do_add_clientid(Params), + case Re of + ok -> return(ok); + <<"ok">> -> return(ok); + _ -> return({error, format_msg(Re)}) + end end. -add_user([ Params | ParamsN ], ReList ) -> - Login = urldecode(get_value(<<"login">>, Params)), - Password = urldecode(get_value(<<"password">>, Params)), - IsSuperuser = get_value(<<"is_superuser">>, Params), - Re = case validate([login, password, is_superuser], [Login, Password, IsSuperuser]) of - ok -> - emqx_auth_mnesia_cli:add_user(Login, Password, IsSuperuser); - Err -> Err - end, - add_user(ParamsN, [{Login, format_msg(Re)} | ReList]); - -add_user([], ReList) -> +do_add_clientid([ Params | ParamsN ], ReList ) -> + Clientid = urldecode(get_value(<<"clientid">>, Params)), + do_add_clientid(ParamsN, [{Clientid, format_msg(do_add_clientid(Params))} | ReList]); + +do_add_clientid([], 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), - IsSuperuser = get_value(<<"is_superuser">>, Params), - case validate([password, is_superuser], [Password, IsSuperuser]) of - ok -> return(emqx_auth_mnesia_cli:update_user(urldecode(Login), urldecode(Password), IsSuperuser)); + case validate([password], [Password]) of + ok -> return(emqx_auth_mnesia_cli:update_user({clientid, urldecode(Clientid)}, urldecode(Password))); Err -> return(Err) end. -delete(#{login := Login}, _) -> - return(emqx_auth_mnesia_cli:remove_user(urldecode(Login))). +delete_clientid(#{clientid := Clientid}, _) -> + 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 %%------------------------------------------------------------------------------ -paginate(Tables, Params, RowFun) -> - Qh = query_handle(Tables), - Count = count(Tables), +paginate(Tables, MatchSpec, Params, ComparingFun, RowFun) -> + Qh = query_handle(Tables, MatchSpec), + Count = count(Tables, MatchSpec), Page = page(Params), Limit = limit(Params), Cursor = qlc:cursor(Qh), @@ -121,21 +227,28 @@ paginate(Tables, Params, RowFun) -> Rows = qlc:next_answers(Cursor, Limit), qlc:delete_cursor(Cursor), #{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) -> - qlc:q([R|| R <- ets:table(Table)]); -query_handle([Table]) when is_atom(Table) -> - qlc:q([R|| R <- ets:table(Table)]); -query_handle(Tables) -> - qlc:append([qlc:q([E || E <- ets:table(T)]) || T <- Tables]). +query_handle(Table, MatchSpec) when is_atom(Table) -> + Options = {traverse, {select, MatchSpec}}, + qlc:q([R|| R <- ets:table(Table, Options)]); +query_handle([Table], MatchSpec) when is_atom(Table) -> + Options = {traverse, {select, MatchSpec}}, + 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) -> - ets:info(Table, size); -count([Table]) when is_atom(Table) -> - ets:info(Table, size); -count(Tables) -> - lists:sum([count(T) || T <- Tables]). +count(Table, MatchSpec) when is_atom(Table) -> + [{MatchPattern, Where, _Re}] = MatchSpec, + NMatchSpec = [{MatchPattern, Where, [true]}], + ets:select_count(Table, NMatchSpec); +count([Table], MatchSpec) when is_atom(Table) -> + [{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) -> binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)). @@ -146,24 +259,28 @@ limit(Params) -> Size -> binary_to_integer(Size) end. - - %%------------------------------------------------------------------------------ %% Interval Funcs %%------------------------------------------------------------------------------ -format({emqx_user, Login, Password, IsSuperuser}) -> - #{login => Login, - password => Password, - is_superuser => IsSuperuser}; +format({?TABLE, {clientid, ClientId}, Password, _InterTime}) -> + #{clientid => ClientId, + password => Password}; + +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([{emqx_user, Login, Password, IsSuperuser}]) -> - #{login => Login, - password => Password, - is_superuser => IsSuperuser}. + #{}. validate([], []) -> ok; @@ -173,14 +290,15 @@ validate([K|Keys], [V|Values]) -> true -> validate(Keys, Values) 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 -> true; do_validation(password, V) when is_binary(V) andalso byte_size(V) > 0 -> true; -do_validation(is_superuser, V) when is_boolean(V) -> - true; do_validation(_, _) -> false. diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl index 094eba0ea..cfc9df995 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_app.erl @@ -34,8 +34,10 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqx_auth_mnesia_sup:start_link(), - emqx_ctl:register_command('mqtt-user', {emqx_auth_mnesia_cli, auth_cli}, []), - emqx_ctl:register_command('mqtt-acl', {emqx_auth_mnesia_cli, acl_cli}, []), + emqx_ctl:register_command(clientid, {emqx_auth_mnesia_cli, auth_clientid_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_acl_hook(), {ok, Sup}. @@ -43,28 +45,26 @@ start(_StartType, _StartArgs) -> prep_stop(State) -> emqx:unhook('client.authenticate', fun emqx_auth_mnesia:check/3), emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5), - emqx_ctl:unregister_command('mqtt-user'), - emqx_ctl:unregister_command('mqtt-acl'), + emqx_ctl:unregister_command(clientid), + emqx_ctl:unregister_command(username), + emqx_ctl:unregister_command(user), + emqx_ctl:unregister_command(acl), State. stop(_State) -> ok. load_auth_hook() -> - DefaultUsers = application:get_env(?APP, userlist, []), - ok = emqx_auth_mnesia:init(DefaultUsers), + ClientidList = application:get_env(?APP, clientid_list, []), + UsernameList = application:get_env(?APP, username_list, []), + ok = emqx_auth_mnesia:init(#{clientid_list => ClientidList, username_list => UsernameList}), ok = emqx_auth_mnesia:register_metrics(), Params = #{ - hash_type => application:get_env(emqx_auth_mnesia, hash_type, sha256), - key_as => application:get_env(emqx_auth_mnesia, as, username) + hash_type => application:get_env(emqx_auth_mnesia, hash_type, sha256) }, emqx:hook('client.authenticate', fun emqx_auth_mnesia:check/3, [Params]). load_acl_hook() -> ok = emqx_acl_mnesia:init(), ok = emqx_acl_mnesia:register_metrics(), - Params = #{ - key_as => application:get_env(emqx_auth_mnesia, as, username) - }, - emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [Params]). - + emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [#{}]). diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl index fb14bed53..35034918c 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_cli.erl @@ -18,31 +18,32 @@ -include("emqx_auth_mnesia.hrl"). -include_lib("emqx_libs/include/logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). -define(TABLE, emqx_user). %% Auth APIs --export([ add_user/3 - , update_user/3 +-export([ add_user/2 + , update_user/2 , remove_user/1 , lookup_user/1 , all_users/0 - ]). -%% Acl APIs --export([ add_acl/4 - , remove_acl/2 - , lookup_acl/1 - , all_acls/0 + , all_users/1 ]). %% Cli --export([ auth_cli/1 - , acl_cli/1]). +-export([ auth_clientid_cli/1 + , auth_username_cli/1 + ]). + +%% Helper +-export([comparing/2]). + %%-------------------------------------------------------------------- %% Auth APIs %%-------------------------------------------------------------------- %% @doc Add User --spec(add_user(binary(), binary(), atom()) -> ok | {error, any()}). -add_user(Login, Password, IsSuperuser) -> - User = #emqx_user{login = Login, password = encrypted_data(Password), is_superuser = IsSuperuser}, +-spec(add_user(tuple(), binary()) -> ok | {error, any()}). +add_user(Login, Password) -> + User = #emqx_user{login = Login, password = encrypted_data(Password), created_at = erlang:system_time(millisecond)}, ret(mnesia:transaction(fun insert_user/1, [User])). insert_user(User = #emqx_user{login = Login}) -> @@ -52,30 +53,31 @@ insert_user(User = #emqx_user{login = Login}) -> end. %% @doc Update User --spec(update_user(binary(), binary(), atom()) -> ok | {error, any()}). -update_user(Login, NewPassword, IsSuperuser) -> - User = #emqx_user{login = Login, password = encrypted_data(NewPassword), is_superuser = IsSuperuser}, +-spec(update_user(tuple(), binary()) -> ok | {error, any()}). +update_user(Login, NewPassword) -> + User = #emqx_user{login = Login, password = encrypted_data(NewPassword)}, ret(mnesia:transaction(fun do_update_user/1, [User])). do_update_user(User = #emqx_user{login = Login}) -> case mnesia:read(?TABLE, Login) of - [_|_] -> mnesia:write(User); + [{?TABLE, Login, _, CreateAt}] -> mnesia:write(User#emqx_user{created_at = CreateAt}); [] -> mnesia:abort(noexisted) end. %% @doc Lookup user by login --spec(lookup_user(binary()) -> list()). +-spec(lookup_user(tuple()) -> list()). lookup_user(undefined) -> []; lookup_user(Login) -> case mnesia:dirty_read(?TABLE, Login) of {error, Reason} -> ?LOG(error, "[Mnesia] do_check_user error: ~p~n", [Reason]), []; - Re -> Re + Re -> + lists:sort(fun comparing/2, Re) end. %% @doc Remove user --spec(remove_user(binary()) -> ok | {error, any()}). +-spec(remove_user(tuple()) -> ok | {error, any()}). remove_user(Login) -> ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, Login}])). @@ -83,111 +85,97 @@ remove_user(Login) -> -spec(all_users() -> list()). all_users() -> mnesia:dirty_all_keys(?TABLE). -%%-------------------------------------------------------------------- -%% Acl API -%%-------------------------------------------------------------------- - -%% @doc Add Acls --spec(add_acl(binary(), binary(), binary(), atom()) -> ok | {error, any()}). -add_acl(Login, Topic, Action, Allow) -> - Acls = #emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}, - ret(mnesia:transaction(fun mnesia:write/1, [Acls])). - -%% @doc Lookup acl by login --spec(lookup_acl(binary()) -> list()). -lookup_acl(undefined) -> []; -lookup_acl(Login) -> - case mnesia:dirty_read(emqx_acl, Login) of - {error, Reason} -> - ?LOG(error, "[Mnesia] do_check_acl error: ~p~n", [Reason]), - []; - Re -> Re - end. - -%% @doc Remove acl --spec(remove_acl(binary(), binary()) -> ok | {error, any()}). -remove_acl(Login, Topic) -> - [ ok = mnesia:dirty_delete_object(emqx_acl, #emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) || [Action, Allow] <- ets:select(emqx_acl, [{{emqx_acl, Login, Topic,'$1','$2'}, [], ['$$']}])], - ok. - - -%% @doc All logins --spec(all_acls() -> list()). -all_acls() -> mnesia:dirty_all_keys(emqx_acl). +all_users(clientid) -> + MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) -> {?TABLE, {clientid, Clientid}, Password, CreatedAt} end), + lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)); +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 %%-------------------------------------------------------------------- +comparing({?TABLE, _, _, CreatedAt1}, + {?TABLE, _, _, CreatedAt2}) -> + CreatedAt1 >= CreatedAt2. + ret({atomic, ok}) -> ok; ret({aborted, Error}) -> {error, Error}. encrypted_data(Password) -> - HashType = application:get_env(emqx_auth_mnesia, hash_type, sha256), - emqx_passwd:hash(HashType, Password). + HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256), + SaltBin = salt(), + <>. + +hash(undefined, SaltBin, HashType) -> + hash(<<>>, SaltBin, HashType); +hash(Password, SaltBin, HashType) -> + emqx_passwd:hash(HashType, <>). + +salt() -> + rand:seed(exsplus, erlang:timestamp()), + Salt = rand:uniform(16#ffffffff), <>. %%-------------------------------------------------------------------- -%% Auth APIs +%% Auth Clientid Cli %%-------------------------------------------------------------------- -%% User -auth_cli(["add", Login, Password, IsSuperuser]) -> - case add_user(iolist_to_binary(Login), iolist_to_binary(Password), IsSuperuser) of +auth_clientid_cli(["list"]) -> + [emqx_ctl:print("~s~n", [ClientId]) || {?TABLE, {clientid, ClientId}, _Password, _CreatedAt} <- all_users(clientid)]; + +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"); {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) end; -auth_cli(["update", Login, NewPassword, IsSuperuser]) -> - case update_user(iolist_to_binary(Login), iolist_to_binary(NewPassword), IsSuperuser) of +auth_clientid_cli(["update", ClientId, NewPassword]) -> + case update_user({clientid, iolist_to_binary(ClientId)}, iolist_to_binary(NewPassword)) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) end; -auth_cli(["del", Login]) -> - case remove_user(iolist_to_binary(Login)) of +auth_clientid_cli(["del", ClientId]) -> + case remove_user({clientid, iolist_to_binary(ClientId)}) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) end; -auth_cli(["show", P]) -> - [emqx_ctl:print("User(login = ~p is_super = ~p)~n", [Login, IsSuperuser]) - || {_, Login, _Password, IsSuperuser} <- lookup_user(iolist_to_binary(P))]; +auth_clientid_cli(_) -> + emqx_ctl:usage([{"clientid list", "List clientid auth rules"}, + {"clientid add ", "Add clientid auth rule"}, + {"clientid update ", "Update clientid auth rule"}, + {"clientid del ", "Delete clientid auth rule"}]). -auth_cli(["list"]) -> - [emqx_ctl:print("User(login = ~p)~n",[E]) - || E <- all_users()]; +%%-------------------------------------------------------------------- +%% Auth Username Cli +%%-------------------------------------------------------------------- -auth_cli(_) -> - emqx_ctl:usage([{"mqtt-user add ", "Add user"}, - {"mqtt-user update ", "Update user"}, - {"mqtt-user delete ", "Delete user"}, - {"mqtt-user show ", "Lookup user detail"}, - {"mqtt-user list", "List all users"}]). +auth_username_cli(["list"]) -> + [emqx_ctl:print("~s~n", [Username]) || {?TABLE, {username, Username}, _Password, _CreatedAt}<- all_users(username)]; -%% Acl -acl_cli(["add", Login, Topic, Action, Allow]) -> - case add_acl(iolist_to_binary(Login), iolist_to_binary(Topic), iolist_to_binary(Action), Allow) of +auth_username_cli(["add", Username, Password]) -> + case add_user({username, iolist_to_binary(Username)}, iolist_to_binary(Password)) of ok -> emqx_ctl:print("ok~n"); {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) end; -acl_cli(["del", Login, Topic])-> - case remove_acl(iolist_to_binary(Login), iolist_to_binary(Topic)) of - ok -> emqx_ctl:print("ok~n"); +auth_username_cli(["update", Username, NewPassword]) -> + case update_user({username, iolist_to_binary(Username)}, iolist_to_binary(NewPassword)) of + ok -> emqx_ctl:print("ok~n"); {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) end; -acl_cli(["show", P]) -> - [emqx_ctl:print("Acl(login = ~p topic = ~p action = ~p allow = ~p)~n",[Login, Topic, Action, Allow]) - || {_, Login, Topic, Action, Allow} <- lookup_acl(iolist_to_binary(P)) ]; +auth_username_cli(["del", Username]) -> + case remove_user({username, iolist_to_binary(Username)}) of + ok -> emqx_ctl:print("ok~n"); + {error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason]) + end; -acl_cli(["list"]) -> - [emqx_ctl:print("Acl(login = ~p)~n",[E]) - || E <- all_acls() ]; - -acl_cli(_) -> - emqx_ctl:usage([{"mqtt-acl add ", "Add acl"}, - {"mqtt-acl show ", "Lookup acl detail"}, - {"mqtt-acl del ", "Delete acl"}, - {"mqtt-acl list","List all acls"}]). +auth_username_cli(_) -> + emqx_ctl:usage([{"users list", "List username auth rules"}, + {"users add ", "Add username auth rule"}, + {"users update ", "Update username auth rule"}, + {"users del ", "Delete username auth rule"}]). diff --git a/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl b/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl index 11cfca521..61356dd24 100644 --- a/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl +++ b/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl @@ -76,154 +76,41 @@ set_special_configs(_App) -> t_management(_Config) -> clean_all_acls(), ?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_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true), - ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/C">>, <<"pubsub">>, true), - - ?assertEqual([{emqx_acl,<<"test_username">>,<<"Topic/A">>,<<"sub">>, true}, - {emqx_acl,<<"test_username">>,<<"Topic/B">>,<<"pub">>, true}, - {emqx_acl,<<"test_username">>,<<"Topic/C">>,<<"pubsub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"test_username">>)), - ok = emqx_auth_mnesia_cli:remove_acl(<<"test_username">>, <<"Topic/A">>), - ?assertEqual([{emqx_acl,<<"test_username">>,<<"Topic/B">>,<<"pub">>, true}, - {emqx_acl,<<"test_username">>,<<"Topic/C">>,<<"pubsub">>, true}], emqx_auth_mnesia_cli:lookup_acl(<<"test_username">>)), + ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>, sub, allow), + ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/+">>, pub, deny), + 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), - - 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), + ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({clientid, <<"test_clientid">>}))), + ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({username, <<"test_username">>}))), + ?assertEqual(1, length(emqx_acl_mnesia_cli:lookup_acl(all))), + ?assertEqual(5, length(emqx_acl_mnesia_cli:all_acls())), 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_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"+/A">>, <<"pub">>, false), - ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"Topic/A/B">>, <<"pubsub">>, true), + ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>), + ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/+">>), + 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">>), - 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">>), + ?assertEqual([], emqx_acl_mnesia_cli:all_acls()). - allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>), - allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>). - -t_check_acl_as_username(_Config) -> - clean_all_acls(), - emqx_modules:load_module(emqx_mod_acl_internal, false), - - User1 = #{zone => external, username => <<"test_username">>}, - User2 = #{zone => external, username => <<"no_exist">>}, - - ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true), - ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true), - ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A/B">>, <<"pubsub">>, false), - allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A">>), - allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/B">>), - deny = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A/B">>), - allow = emqx_access_control:check_acl(User1, publish, <<"Topic/A">>), - allow = emqx_access_control:check_acl(User1, publish, <<"Topic/B">>), - deny = emqx_access_control:check_acl(User1, publish, <<"Topic/A/B">>), - - allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>), - allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>). - -t_check_acl_as_all(_) -> - clean_all_acls(), - emqx_modules:load_module(emqx_mod_acl_internal, false), - - ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/A">>, <<"sub">>, false), - ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/B">>, <<"pub">>, false), - ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/A/B">>, <<"pubsub">>, true), - - User1 = #{zone => external, username => <<"test_username">>}, - User2 = #{zone => external, username => <<"no_exist">>}, - - ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true), - ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true), - ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A/B">>, <<"pubsub">>, false), - - allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A">>), - allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/B">>), - deny = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A/B">>), - allow = emqx_access_control:check_acl(User1, publish, <<"Topic/A">>), - allow = emqx_access_control:check_acl(User1, publish, <<"Topic/B">>), - deny = emqx_access_control:check_acl(User1, publish, <<"Topic/A/B">>), - - deny = emqx_access_control:check_acl(User2, subscribe, <<"Topic/A">>), - deny = emqx_access_control:check_acl(User2, publish, <<"Topic/B">>), - allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/A/B">>), - allow = emqx_access_control:check_acl(User2, publish, <<"Topic/A/B">>), - allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>), - allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>). - -t_rest_api(_Config) -> - clean_all_acls(), - - {ok, Result} = request_http_rest_list(), - [] = get_http_data(Result), - - Params = #{<<"login">> => <<"test_username">>, <<"topic">> => <<"Topic/A">>, <<"action">> => <<"pubsub">>, <<"allow">> => true}, - {ok, _} = request_http_rest_add(Params), - {ok, Result1} = request_http_rest_lookup(<<"test_username">>), - #{<<"login">> := <<"test_username">>, <<"topic">> := <<"Topic/A">>, <<"action">> := <<"pubsub">>, <<"allow">> := true} = get_http_data(Result1), - - Params1 = [ - #{<<"login">> => <<"$all">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"pub">>, <<"allow">> => true}, - #{<<"login">> => <<"test_username">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"pub">>, <<"allow">> => true}, - #{<<"login">> => <<"test_username/1">>, <<"topic">> => <<"#">>, <<"action">> => <<"sub">>, <<"allow">> => true}, - #{<<"login">> => <<"test_username/2">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"error_format">>, <<"allow">> => true} - ], - {ok, Result2} = request_http_rest_add(Params1), - #{ - <<"$all">> := <<"ok">>, - <<"test_username">> := <<"ok">>, - <<"test_username/1">> := <<"ok">>, - <<"test_username/2">> := <<"{error,action}">> - } = get_http_data(Result2), - - {ok, Result3} = request_http_rest_lookup(<<"test_username">>), - [#{<<"login">> := <<"test_username">>, <<"topic">> := <<"+/A">>, <<"action">> := <<"pub">>, <<"allow">> := true}, - #{<<"login">> := <<"test_username">>, <<"topic">> := <<"Topic/A">>, <<"action">> := <<"pubsub">>, <<"allow">> := true}] - = get_http_data(Result3), - - {ok, Result4} = request_http_rest_lookup(<<"$all">>), - #{<<"login">> := <<"$all">>, <<"topic">> := <<"+/A">>, <<"action">> := <<"pub">>, <<"allow">> := true} - = get_http_data(Result4), - - {ok, _} = request_http_rest_delete(<<"$all">>, <<"+/A">>), - {ok, _} = request_http_rest_delete(<<"test_username">>, <<"+/A">>), - {ok, _} = request_http_rest_delete(<<"test_username">>, <<"Topic/A">>), - {ok, _} = request_http_rest_delete(<<"test_username/1">>, <<"#">>), - {ok, Result5} = request_http_rest_list(), - [] = get_http_data(Result5). - - -t_run_command(_) -> - clean_all_acls(), - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "add", "TestUser", "Topic/A", "sub", true])), - ?assertEqual([{emqx_acl,<<"TestUser">>,<<"Topic/A">>,<<"sub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"TestUser">>)), - - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "del", "TestUser", "Topic/A"])), - ?assertEqual([],emqx_auth_mnesia_cli:lookup_acl(<<"TestUser">>)), - - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "show", "TestUser"])), - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "list"])), - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl"])). - -t_cli(_) -> +t_acl_cli(_Config) -> 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), @@ -231,18 +118,67 @@ t_cli(_) -> meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end), clean_all_acls(), - ?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:acl_cli(["add", "TestUser", "Topic/A", "sub", true]), "ok")), - ?assertMatch(["Acl(login = <<\"TestUser\">> topic = <<\"Topic/A\">> action = <<\"sub\">> allow = true)\n"], emqx_auth_mnesia_cli:acl_cli(["show", "TestUser"])), - ?assertMatch(["Acl(login = <<\"TestUser\">>)\n"], emqx_auth_mnesia_cli:acl_cli(["list"])), - ?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:acl_cli(["del", "TestUser", "Topic/A"]), "ok")), - ?assertMatch([], emqx_auth_mnesia_cli:acl_cli(["show", "TestUser"])), - ?assertMatch([], emqx_auth_mnesia_cli:acl_cli(["list"])), + ?assertEqual(0, length(emqx_acl_mnesia_cli: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). +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 %%------------------------------------------------------------------------------ @@ -255,22 +191,22 @@ clean_all_acls() -> %% HTTP Request %%-------------------------------------------------------------------- -request_http_rest_list() -> - request_api(get, uri(), default_auth_header()). +request_http_rest_list(Path) -> + request_api(get, uri(Path), default_auth_header()). -request_http_rest_lookup(Login) -> - request_api(get, uri([Login]), default_auth_header()). +request_http_rest_lookup(Path) -> + request_api(get, uri(Path), default_auth_header()). -request_http_rest_add(Params) -> - request_api(post, uri(), [], default_auth_header(), Params). +request_http_rest_add(Path, Params) -> + request_api(post, uri(Path), [], default_auth_header(), Params). -request_http_rest_delete(Login, Topic) -> - request_api(delete, uri([Login, Topic]), default_auth_header()). +request_http_rest_delete(Path) -> + request_api(delete, uri(Path), default_auth_header()). uri() -> uri([]). uri(Parts) when is_list(Parts) -> NParts = [b2l(E) || E <- Parts], - ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "mqtt_acl"| NParts]). + ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "acl"| NParts]). %% @private b2l(B) when is_binary(B) -> diff --git a/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl b/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl index 8979f4755..0f4884247 100644 --- a/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl +++ b/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl @@ -2,10 +2,7 @@ %% 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 +%% 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, @@ -33,6 +30,12 @@ -define(API_VERSION, "v4"). -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() -> emqx_ct:all(?MODULE). @@ -81,143 +84,166 @@ set_special_configs(_App) -> %% Testcases %%------------------------------------------------------------------------------ -t_check_as_username(_Config) -> +t_management(_Config) -> clean_all_users(), - ok = emqx_auth_mnesia_cli:add_user(<<"test_username">>, <<"password">>, true), - {error, existed} = emqx_auth_mnesia_cli:add_user(<<"test_username">>, <<"password">>, true), + ok = emqx_auth_mnesia_cli:add_user({username,?USERNAME}, ?PASSWORD), + {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), - {error,noexisted} = emqx_auth_mnesia_cli:update_user(<<"no_existed_user">>, <<"password">>, true), + ok = emqx_auth_mnesia_cli:add_user({clientid,?CLIENTID}, ?PASSWORD), + {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(), - [{emqx_user, <<"test_username">>, _HashedPass, false}] = - emqx_auth_mnesia_cli:lookup_user(<<"test_username">>), + ?assertEqual(2,length(emqx_auth_mnesia_cli:all_users())), - User1 = #{username => <<"test_username">>, - password => <<"new_password">>, + ok = emqx_auth_mnesia_cli:update_user({username,?USERNAME}, ?NPASSWORD), + {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}, - {ok, #{is_superuser := false, - auth_result := success, + {ok, #{auth_result := success, anonymous := false}} = emqx_access_control:authenticate(User1), {error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}), - ok = emqx_auth_mnesia_cli:remove_user(<<"test_username">>), + ok = emqx_auth_mnesia_cli:remove_user({username,?USERNAME}), {ok, #{auth_result := success, - anonymous := true }} = emqx_access_control:authenticate(User1). + anonymous := true }} = emqx_access_control:authenticate(User1), -t_check_as_clientid(_Config) -> - clean_all_users(), - - ok = emqx_auth_mnesia_cli:add_user(<<"test_clientid">>, <<"password">>, false), - {error, existed} = emqx_auth_mnesia_cli:add_user(<<"test_clientid">>, <<"password">>, false), - - ok = emqx_auth_mnesia_cli:update_user(<<"test_clientid">>, <<"new_password">>, true), - {error,noexisted} = emqx_auth_mnesia_cli:update_user(<<"no_existed_user">>, <<"password">>, true), - - [<<"test_clientid">>] = emqx_auth_mnesia_cli:all_users(), - [{emqx_user, <<"test_clientid">>, _HashedPass, true}] = - emqx_auth_mnesia_cli:lookup_user(<<"test_clientid">>), - - User1 = #{clientid => <<"test_clientid">>, - password => <<"new_password">>, + User2 = #{clientid => ?CLIENTID, + password => ?NPASSWORD, zone => external}, - {ok, #{is_superuser := true, - auth_result := success, - anonymous := false}} = emqx_access_control:authenticate(User1), - - {error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}), - - ok = emqx_auth_mnesia_cli:remove_user(<<"test_clientid">>), {ok, #{auth_result := success, - anonymous := true }} = emqx_access_control:authenticate(User1). + 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(), - {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}, <>, _}] = emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}), + ?assertEqual(Hash, emqx_passwd:hash(HashType, <>)), + + emqx_auth_mnesia_cli:auth_clientid_cli(["update", ?CLIENTID, ?NPASSWORD]), + [{_, {clientid, ?CLIENTID}, <>, _}] = emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}), + ?assertEqual(Hash1, emqx_passwd:hash(HashType, <>)), + + 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}, <>, _}] = emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}), + ?assertEqual(Hash, emqx_passwd:hash(HashType, <>)), + + emqx_auth_mnesia_cli:auth_username_cli(["update", ?USERNAME, ?NPASSWORD]), + [{_, {username, ?USERNAME}, <>, _}] = emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}), + ?assertEqual(Hash1, emqx_passwd:hash(HashType, <>)), + + 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), - Params = #{<<"login">> => <<"test_username">>, <<"password">> => <<"password">>, <<"is_superuser">> => true}, - {ok, _} = request_http_rest_add(Params), + Params1 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD}, + {ok, _} = request_http_rest_add(["auth_clientid"], Params1), - Params1 = [ - #{<<"login">> => <<"test_username">>, <<"password">> => <<"password">>, <<"is_superuser">> => true}, - #{<<"login">> => <<"test_username/1">>, <<"password">> => <<"password">>, <<"is_superuser">> => error_format}, - #{<<"login">> => <<"test_username/2">>, <<"password">> => <<"password">>, <<"is_superuser">> => true} - ], - {ok, Result2} = request_http_rest_add(Params1), - #{ - <<"test_username">> := <<"{error,existed}">>, - <<"test_username/1">> := <<"{error,is_superuser}">>, - <<"test_username/2">> := <<"ok">> - } = get_http_data(Result2), + Params2 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?NPASSWORD}, + {ok, _} = request_http_rest_update(["auth_clientid/" ++ binary_to_list(?CLIENTID)], Params2), + + {ok, Result2} = request_http_rest_lookup(["auth_clientid/" ++ binary_to_list(?CLIENTID)]), + ?assertMatch(#{<<"clientid">> := ?CLIENTID}, get_http_data(Result2)), - {ok, Result3} = request_http_rest_lookup(<<"test_username">>), - #{<<"login">> := <<"test_username">>, <<"is_superuser">> := true} = get_http_data(Result3), + Params3 = [ #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD} + , #{<<"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">>, error_format), - {ok, _} = request_http_rest_update(<<"error_username">>, <<"new_password">>, false), + {ok, Result4} = request_http_rest_list(["auth_clientid"]), + ?assertEqual(3, length(get_http_data(Result4))), - {ok, _} = request_http_rest_update(<<"test_username">>, <<"new_password">>, false), - {ok, Result4} = request_http_rest_lookup(<<"test_username">>), - #{<<"login">> := <<"test_username">>, <<"is_superuser">> := false} = get_http_data(Result4), - - User1 = #{username => <<"test_username">>, - password => <<"new_password">>, - zone => external}, - - {ok, #{is_superuser := false, - auth_result := success, - anonymous := false}} = emqx_access_control:authenticate(User1), - - {ok, _} = request_http_rest_delete(<<"test_username">>), - {ok, #{auth_result := success, - anonymous := true }} = emqx_access_control:authenticate(User1). - -t_run_command(_) -> - clean_all_users(), - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "add", "TestUser", "Password", false])), - ?assertMatch([{emqx_user, <<"TestUser">>, _, false}], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)), - - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "update", "TestUser", "NewPassword", true])), - ?assertMatch([{emqx_user, <<"TestUser">>, _, true}], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)), - - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "del", "TestUser"])), - ?assertMatch([], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)), - - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "show", "TestUser"])), - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "list"])), - ?assertEqual(ok, emqx_ctl:run_command(["mqtt-user"])). - -t_cli(_) -> - meck:new(emqx_ctl, [non_strict, passthrough]), - meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end), - meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end), - meck:expect(emqx_ctl, usage, fun(Usages) -> emqx_ctl:format_usage(Usages) end), - meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end), + {ok, _} = request_http_rest_delete(["auth_clientid/" ++ binary_to_list(?CLIENTID)]), + {ok, Result5} = request_http_rest_lookup(["auth_clientid/" ++ binary_to_list(?CLIENTID)]), + ?assertMatch(#{}, get_http_data(Result5)). +t_username_rest_api(_Config) -> clean_all_users(), - ?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["add", "TestUser", "Password", true]), "ok")), - ?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["add", "TestUser", "Password", true]), "Error")), + {ok, Result1} = request_http_rest_list(["auth_username"]), + [] = get_http_data(Result1), - ?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["update", "NoExisted", "Password", false]), "Error")), - ?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["update", "TestUser", "Password", false]), "ok")), + Params1 = #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD}, + {ok, _} = request_http_rest_add(["auth_username"], Params1), - ?assertMatch(["User(login = <<\"TestUser\">> is_super = false)\n"], emqx_auth_mnesia_cli:auth_cli(["show", "TestUser"])), - ?assertMatch(["User(login = <<\"TestUser\">>)\n"], emqx_auth_mnesia_cli:auth_cli(["list"])), + Params2 = #{<<"username">> => ?USERNAME, <<"password">> => ?NPASSWORD}, + {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")), - ?assertMatch([], emqx_auth_mnesia_cli:auth_cli(["show", "TestUser"])), - ?assertMatch([], emqx_auth_mnesia_cli:auth_cli(["list"])), + {ok, Result2} = request_http_rest_lookup(["auth_username/" ++ binary_to_list(?USERNAME)]), + ?assertMatch(#{<<"username">> := ?USERNAME}, get_http_data(Result2)), - ?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 @@ -231,18 +257,17 @@ clean_all_users() -> %% HTTP Request %%-------------------------------------------------------------------- -request_http_rest_list() -> - request_api(get, uri(), default_auth_header()). +request_http_rest_list(Path) -> + request_api(get, uri(Path), default_auth_header()). -request_http_rest_lookup(Login) -> - request_api(get, uri([Login]), default_auth_header()). +request_http_rest_lookup(Path) -> + request_api(get, uri([Path]), default_auth_header()). -request_http_rest_add(Params) -> - request_api(post, uri(), [], default_auth_header(), Params). +request_http_rest_add(Path, Params) -> + request_api(post, uri(Path), [], default_auth_header(), Params). -request_http_rest_update(Login, Password, IsSuperuser) -> - Params = #{<<"password">> => Password, <<"is_superuser">> => IsSuperuser}, - request_api(put, uri([Login]), [], default_auth_header(), Params). +request_http_rest_update(Path, Params) -> + request_api(put, uri([Path]), [], default_auth_header(), Params). request_http_rest_delete(Login) -> request_api(delete, uri([Login]), default_auth_header()). @@ -250,7 +275,7 @@ request_http_rest_delete(Login) -> uri() -> uri([]). uri(Parts) when is_list(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 b2l(B) when is_binary(B) -> diff --git a/apps/emqx_auth_username/README.md b/apps/emqx_auth_username/README.md deleted file mode 100644 index bc0dd6e04..000000000 --- a/apps/emqx_auth_username/README.md +++ /dev/null @@ -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. - diff --git a/apps/emqx_auth_username/TODO b/apps/emqx_auth_username/TODO deleted file mode 100644 index c968cc25a..000000000 --- a/apps/emqx_auth_username/TODO +++ /dev/null @@ -1 +0,0 @@ -Upgrade test cases for 3.0 diff --git a/apps/emqx_auth_username/include/emqx_auth_username.hrl b/apps/emqx_auth_username/include/emqx_auth_username.hrl deleted file mode 100644 index b70129858..000000000 --- a/apps/emqx_auth_username/include/emqx_auth_username.hrl +++ /dev/null @@ -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)). - diff --git a/apps/emqx_auth_username/priv/emqx_auth_username.schema b/apps/emqx_auth_username/priv/emqx_auth_username.schema deleted file mode 100644 index 6e449fee6..000000000 --- a/apps/emqx_auth_username/priv/emqx_auth_username.schema +++ /dev/null @@ -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}. - diff --git a/apps/emqx_auth_username/rebar.config b/apps/emqx_auth_username/rebar.config deleted file mode 100644 index 7b30a8fd8..000000000 --- a/apps/emqx_auth_username/rebar.config +++ /dev/null @@ -1 +0,0 @@ -{deps, []}. diff --git a/apps/emqx_auth_username/src/emqx_auth_username.app.src b/apps/emqx_auth_username/src/emqx_auth_username.app.src deleted file mode 100644 index c48c1de24..000000000 --- a/apps/emqx_auth_username/src/emqx_auth_username.app.src +++ /dev/null @@ -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 "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-auth-username"} - ]} - ]}. diff --git a/apps/emqx_auth_username/src/emqx_auth_username.erl b/apps/emqx_auth_username/src/emqx_auth_username.erl deleted file mode 100644 index b5c089b49..000000000 --- a/apps/emqx_auth_username/src/emqx_auth_username.erl +++ /dev/null @@ -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 ", "Add User"}, - {"users update ", "Update User"}, - {"users del ", "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 = <>}] -> - 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(), - <>. - -hash(undefined, SaltBin, HashType) -> - hash(<<>>, SaltBin, HashType); -hash(Password, SaltBin, HashType) -> - emqx_passwd:hash(HashType, <>). - -salt() -> - rand:seed(exsplus, erlang:timestamp()), - Salt = rand:uniform(16#ffffffff), <>. - diff --git a/apps/emqx_auth_username/src/emqx_auth_username_api.erl b/apps/emqx_auth_username/src/emqx_auth_username_api.erl deleted file mode 100644 index 725a7471d..000000000 --- a/apps/emqx_auth_username/src/emqx_auth_username_api.erl +++ /dev/null @@ -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. diff --git a/apps/emqx_auth_username/src/emqx_auth_username_app.erl b/apps/emqx_auth_username/src/emqx_auth_username_app.erl deleted file mode 100644 index 5308d2cdb..000000000 --- a/apps/emqx_auth_username/src/emqx_auth_username_app.erl +++ /dev/null @@ -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}, []} }. - diff --git a/apps/emqx_auth_username/test/emqx_auth_username_SUITE.erl b/apps/emqx_auth_username/test/emqx_auth_username_SUITE.erl deleted file mode 100644 index 872f0c3e0..000000000 --- a/apps/emqx_auth_username/test/emqx_auth_username_SUITE.erl +++ /dev/null @@ -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, <>}] = - emqx_auth_username:lookup_user(Username), - Hash =:= emqx_passwd:hash(HashType, <>). - -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}}. - diff --git a/rebar.config b/rebar.config index 57928cef0..2ea0e984b 100644 --- a/rebar.config +++ b/rebar.config @@ -119,8 +119,6 @@ , {emqx_sn, load} , {emqx_coap, load} , {emqx_stomp, load} - , {emqx_auth_clientid, load} - , {emqx_auth_username, load} , {emqx_auth_http, load} , {emqx_auth_mysql, load} , {emqx_auth_jwt, load}