diff --git a/CHANGES-4.3.md b/CHANGES-4.3.md index 49bee148e..8b509fe17 100644 --- a/CHANGES-4.3.md +++ b/CHANGES-4.3.md @@ -21,6 +21,7 @@ File format: * Return a client_identifier_not_valid error when username is empty and username_as_clientid is set to true [#7862] * Add more rule engine date functions: format_date/3, format_date/4, date_to_unix_ts/4 [#7894] * Add proto_name and proto_ver fields for $event/client_disconnected event. +* Mnesia auth/acl http api support multiple condition queries. ### Bug fixes * List subscription topic (/api/v4/subscriptions), the result do not match with multiple conditions. 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 a88b83d0c..163abf075 100644 --- a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl +++ b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_api.erl @@ -96,18 +96,24 @@ , delete/2 ]). +-define(CLIENTID_SCHEMA, [{<<"clientid">>, binary}, {<<"_like_clientid">>, binary}] ++ ?COMMON_SCHEMA). +-define(USERNAME_SCHEMA, [{<<"username">>, binary}, {<<"_like_username">>, binary}] ++ ?COMMON_SCHEMA). +-define(COMMON_SCHEMA, [{<<"topic">>, binary}, {<<"action">>, atom}, {<<"access">>, atom}]). + list_clientid(_Bindings, Params) -> - Table = emqx_acl_mnesia_db:login_acl_table(clientid), - return({ok, emqx_auth_mnesia_api:paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). + {_, Params1 = {_Qs, _Fuzzy}} = emqx_mgmt_api:params2qs(Params, ?CLIENTID_SCHEMA), + Table = emqx_acl_mnesia_db:login_acl_table(clientid, Params1), + return({ok, paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). list_username(_Bindings, Params) -> - Table = emqx_acl_mnesia_db:login_acl_table(username), - return({ok, emqx_auth_mnesia_api:paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). + {_, Params1 = {_Qs, _Fuzzy}} = emqx_mgmt_api:params2qs(Params, ?USERNAME_SCHEMA), + Table = emqx_acl_mnesia_db:login_acl_table(username, Params1), + return({ok, paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). list_all(_Bindings, Params) -> - Table = emqx_acl_mnesia_db:login_acl_table(all), - return({ok, emqx_auth_mnesia_api:paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). - + {_, Params1 = {_Qs, _Fuzzy}} = emqx_mgmt_api:params2qs(Params, ?COMMON_SCHEMA), + Table = emqx_acl_mnesia_db:login_acl_table(all, Params1), + return({ok, paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). lookup(#{clientid := Clientid}, _Params) -> return({ok, format(emqx_acl_mnesia_db:lookup_acl({clientid, urldecode(Clientid)}))}); @@ -170,7 +176,11 @@ delete(#{topic := Topic}, _) -> %%------------------------------------------------------------------------------ count(QH) -> - qlc:fold(fun(_, Count) -> Count + 1 end, 0, QH). + Count = qlc:fold(fun(_, Sum) -> Sum + 1 end, 0, QH), + case is_integer(Count) of + true -> Count; + false -> 0 + end. format({{clientid, Clientid}, Topic, Action, Access, _CreatedAt}) -> #{clientid => Clientid, topic => Topic, action => Action, access => Access}; @@ -222,3 +232,27 @@ format_msg(Message) when is_tuple(Message) -> urldecode(S) -> emqx_http_lib:uri_decode(S). + +paginate_qh(Qh, Count, Params, ComparingFun, RowFun) -> + Page = page(Params), + Limit = limit(Params), + Cursor = qlc:cursor(Qh), + case Page > 1 of + true -> + _ = qlc:next_answers(Cursor, (Page - 1) * Limit), + ok; + false -> ok + end, + Rows = qlc:next_answers(Cursor, Limit), + qlc:delete_cursor(Cursor), + #{meta => #{page => Page, limit => Limit, count => Count}, + data => [RowFun(Row) || Row <- lists:sort(ComparingFun, Rows)]}. + +page(Params) -> + binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)). + +limit(Params) -> + case proplists:get_value(<<"_limit">>, Params) of + undefined -> 50; + Size -> binary_to_integer(Size) + end. diff --git a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_db.erl b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_db.erl index 23f2447bc..8fcbb8f9a 100644 --- a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_db.erl +++ b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_db.erl @@ -33,6 +33,7 @@ , remove_acl/2 , merge_acl_records/3 , login_acl_table/1 + , login_acl_table/2 , is_migration_started/0 ]). @@ -124,7 +125,7 @@ all_acls_export() -> {atomic, Records} = mnesia:transaction( fun() -> - QH = acl_table(MatchSpecNew, MatchSpecOld, fun mnesia:table/2, fun lookup_mnesia/2), + QH = acl_table(MatchSpecNew, MatchSpecOld, {#{}, #{}}, fun mnesia:table/2, fun lookup_mnesia/2), qlc:eval(QH) end), Records. @@ -132,9 +133,15 @@ all_acls_export() -> %% @doc QLC table of logins matching spec -spec(login_acl_table(acl_target_type()) -> qlc:query_handle()). login_acl_table(AclTargetType) -> - MatchSpecNew = login_match_spec_new(AclTargetType), - MatchSpecOld = login_match_spec_old(AclTargetType), - acl_table(MatchSpecNew, MatchSpecOld, fun ets:table/2, fun lookup_ets/2). + login_acl_table(AclTargetType, {[], []}). + +login_acl_table(AclTargetType, {Qs, Fuzzy}) -> + ToMap = fun({Type, Symbol, Val}, Acc) -> Acc#{{Type, Symbol} => Val} end, + Qs1 = lists:foldl(ToMap, #{}, Qs), + Fuzzy1 = lists:foldl(ToMap, #{}, Fuzzy), + MatchSpecNew = login_match_spec_new(AclTargetType, Qs1), + MatchSpecOld = login_match_spec_old(AclTargetType, Qs1), + acl_table(MatchSpecNew, MatchSpecOld, {Qs1, Fuzzy1}, fun ets:table/2, fun lookup_ets/2). %% @doc Combine old `emqx_acl` ACL records with a new `emqx_acl2` ACL record for a given login -spec(merge_acl_records(acl_target(), [#?ACL_TABLE{}], [#?ACL_TABLE2{}]) -> #?ACL_TABLE2{}). @@ -223,27 +230,39 @@ comparing({_, _, _, _, CreatedAt1}, {_, _, _, _, CreatedAt2}) -> CreatedAt1 >= CreatedAt2. -login_match_spec_old(all) -> +login_match_spec_old(Type) -> login_match_spec_old(Type, #{}). + +login_match_spec_old(all, _) -> ets:fun2ms(fun(#?ACL_TABLE{filter = {all, _}} = Record) -> Record end); -login_match_spec_old(Type) when (Type =:= username) or (Type =:= clientid) -> - ets:fun2ms(fun(#?ACL_TABLE{filter = {{RecordType, _}, _}} = Record) - when RecordType =:= Type -> Record - end). +login_match_spec_old(Type, Params) when (Type =:= username) orelse (Type =:= clientid) -> + case maps:get({Type, '=:='}, Params, undefined) of + undefined -> + ets:fun2ms(fun(#?ACL_TABLE{filter = {{RType, _}, _}} = Rec) when RType =:= Type -> Rec end); + Val -> + ets:fun2ms(fun(#?ACL_TABLE{filter = {{RType, RVal}, _}} = Rec) + when RType =:= Type andalso RVal =:= Val -> Rec end) + end. -login_match_spec_new(all) -> +login_match_spec_new(Type) -> login_match_spec_new(Type, #{}). + +login_match_spec_new(all, _) -> ets:fun2ms(fun(#?ACL_TABLE2{who = all} = Record) -> Record end); -login_match_spec_new(Type) when (Type =:= username) or (Type =:= clientid) -> - ets:fun2ms(fun(#?ACL_TABLE2{who = {RecordType, _}} = Record) - when RecordType =:= Type -> Record - end). +login_match_spec_new(Type, Params) when (Type =:= username) orelse (Type =:= clientid) -> + case maps:get({Type, '=:='}, Params, undefined) of + undefined -> + ets:fun2ms(fun(#?ACL_TABLE2{who = {RType, _}} = Rec) when RType =:= Type -> Rec end); + Val -> + ets:fun2ms(fun(#?ACL_TABLE2{who = {RType, RVal}} = Rec) + when RType =:= Type andalso RVal =:= Val -> Rec end) + end. -acl_table(MatchSpecNew, MatchSpecOld, TableFun, LookupFun) -> +acl_table(MatchSpecNew, MatchSpecOld, Params, TableFun, LookupFun) -> TraverseFun = fun() -> CursorNew = @@ -252,7 +271,7 @@ acl_table(MatchSpecNew, MatchSpecOld, TableFun, LookupFun) -> CursorOld = qlc:cursor( TableFun(?ACL_TABLE, [{traverse, {select, MatchSpecOld}}])), - traverse_new(CursorNew, CursorOld, #{}, LookupFun) + traverse_new(CursorNew, CursorOld, Params, #{}, LookupFun) end, qlc:table(TraverseFun, []). @@ -265,12 +284,12 @@ acl_table(MatchSpecNew, MatchSpecOld, TableFun, LookupFun) -> % After migration, number of such logins is zero, so traversing starts working in % constant memory. -traverse_new(CursorNew, CursorOld, FoundKeys, LookupFun) -> +traverse_new(CursorNew, CursorOld, Params, FoundKeys, LookupFun) -> Acls = qlc:next_answers(CursorNew, 1), case Acls of [] -> qlc:delete_cursor(CursorNew), - traverse_old(CursorOld, FoundKeys); + traverse_old(CursorOld, Params, FoundKeys); [#?ACL_TABLE2{who = Login, rules = Rules} = Acl] -> Keys = lists:usort([{Login, Topic} || {_, _, Topic, _} <- Rules]), OldRecs = lists:flatmap(fun(Key) -> LookupFun(?ACL_TABLE, Key) end, Keys), @@ -281,27 +300,57 @@ traverse_new(CursorNew, CursorOld, FoundKeys, LookupFun) -> OldRecs), case acl_to_list(MergedAcl) of [] -> - traverse_new(CursorNew, CursorOld, NewFoundKeys, LookupFun); + traverse_new(CursorNew, CursorOld, Params, NewFoundKeys, LookupFun); List -> - List ++ fun() -> traverse_new(CursorNew, CursorOld, NewFoundKeys, LookupFun) end + filter_params(List, Params) ++ + fun() -> traverse_new(CursorNew, CursorOld, Params, NewFoundKeys, LookupFun) end end end. -traverse_old(CursorOld, FoundKeys) -> +filter_params(List, {Qs, Fuzzy}) -> + case maps:size(Qs) =:= 0 andalso maps:size(Fuzzy) =:= 0 of + false -> + Topic = maps:get({topic, '=:='}, Qs, undefined), + Action = maps:get({action, '=:='}, Qs, undefined), + Access = maps:get({access, '=:='}, Qs, undefined), + lists:filter(fun({Target, Topic0, Action0, Access0, _CreatedAt}) -> + CheckList = [{Topic, Topic0}, {Action, Action0}, {Access, Access0}], + case lists:all(fun is_match/1, CheckList) of + true -> + case Target of + {Type, Login} -> + case maps:get({Type, 'like'}, Fuzzy, <<>>) of + <<>> -> true; + LikeSchema -> binary:match(Login, LikeSchema) =/= nomatch + end; + all -> true + end; + false -> false + end + end, List); + true -> List + end. + +is_match({Schema, Val}) -> + Schema =:= undefined orelse Schema =:= Val. + +traverse_old(CursorOld, Params, FoundKeys) -> OldAcls = qlc:next_answers(CursorOld), case OldAcls of [] -> qlc:delete_cursor(CursorOld), []; _ -> - Records = [ {Login, Topic, Action, Access, CreatedAt} + Records = [{Login, Topic, Action, Access, CreatedAt} || #?ACL_TABLE{filter = {Login, Topic}, action = LegacyAction, access = Access, created_at = CreatedAt} <- OldAcls, {_, Action, _, _} <- normalize_rule({Access, LegacyAction, Topic, CreatedAt}), not maps:is_key({Login, Topic}, FoundKeys) ], case Records of - [] -> traverse_old(CursorOld, FoundKeys); - List -> List ++ fun() -> traverse_old(CursorOld, FoundKeys) end + [] -> traverse_old(CursorOld, Params, FoundKeys); + List -> + filter_params(List, Params) + ++ fun() -> traverse_old(CursorOld, Params, FoundKeys) end end end. 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 d8968c05f..592535f46 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src @@ -1,6 +1,6 @@ {application, emqx_auth_mnesia, [{description, "EMQ X Authentication with Mnesia"}, - {vsn, "4.3.6"}, % strict semver, bump manually + {vsn, "4.3.7"}, % strict semver, bump manually {modules, []}, {registered, []}, {applications, [kernel,stdlib,mnesia]}, diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src index f4c6ebadd..a9e47eb65 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src @@ -1,9 +1,10 @@ %% -*- mode: erlang -*- %% Unless you know what you are doing, DO NOT edit manually!! {VSN, - [{"4.3.5", - [{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, - {load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]}]}, + [{<<"4\\.3\\.[5-6]">>, + [{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, + {load_module,emqx_acl_mnesia_db,brutal_purge,soft_purge,[]}, + {load_module,emqx_acl_mnesia_api,brutal_purge,soft_purge,[]}]}, {<<"4\\.3\\.[0-3]">>, [{load_module,emqx_auth_mnesia_cli,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]}, @@ -19,13 +20,16 @@ {"4.3.4", [{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]}, + {load_module,emqx_acl_mnesia_db,brutal_purge,soft_purge,[]}, + {load_module,emqx_acl_mnesia_api,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia_cli,brutal_purge,soft_purge,[]}, {load_module,emqx_acl_mnesia,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia_app,brutal_purge,soft_purge,[]}]}, {<<".*">>,[]}], - [{"4.3.5", + [{<<"4\\.3\\.[5-6]">>, [{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, - {load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]}]}, + {load_module,emqx_acl_mnesia_db,brutal_purge,soft_purge,[]}, + {load_module,emqx_acl_mnesia_api,brutal_purge,soft_purge,[]}]}, {<<"4\\.3\\.[0-3]">>, [{load_module,emqx_auth_mnesia_cli,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]}, @@ -40,8 +44,10 @@ {delete_module,emqx_acl_mnesia_db}]}, {"4.3.4", [{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, + {load_module,emqx_acl_mnesia_api,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia_cli,brutal_purge,soft_purge,[]}, + {load_module,emqx_acl_mnesia_db,brutal_purge,soft_purge,[]}, {load_module,emqx_acl_mnesia,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia_app,brutal_purge,soft_purge,[]}]}, {<<".*">>,[]}]}. 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 e400452ec..331d14a92 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia_api.erl @@ -18,18 +18,20 @@ -include_lib("stdlib/include/qlc.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). +-include("emqx_auth_mnesia.hrl"). -define(TABLE, emqx_user). -import(proplists, [get_value/2]). -import(minirest, [return/1]). --export([paginate_qh/5]). -export([ list_clientid/2 , lookup_clientid/2 , add_clientid/2 , update_clientid/2 , delete_clientid/2 + , query_clientid/3 + , query_username/3 ]). -rest_api(#{name => list_clientid, @@ -109,13 +111,28 @@ descr => "Delete username in the cluster" }). +-define(CLIENTID_SCHEMA, {?TABLE, + [ + {<<"clientid">>, binary}, + {<<"_like_clientid">>, binary} + ]}). + +-define(USERNAME_SCHEMA, {?TABLE, + [ + {<<"username">>, binary}, + {<<"_like_username">>, binary} + ]}). + +-define(query_clientid, {?MODULE, query_clientid}). +-define(query_username, {?MODULE, query_username}). + %%------------------------------------------------------------------------------ %% 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)}). + SortFun = fun(#{created_at := C1}, #{created_at := C2}) -> C1 > C2 end, + return({ok, emqx_mgmt_api:node_query(node(), Params, ?CLIENTID_SCHEMA, ?query_clientid, SortFun)}). lookup_clientid(#{clientid := Clientid}, _Params) -> return({ok, format(emqx_auth_mnesia_cli:lookup_user({clientid, urldecode(Clientid)}))}). @@ -164,8 +181,8 @@ delete_clientid(#{clientid := Clientid}, _) -> %%------------------------------------------------------------------------------ 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)}). + SortFun = fun(#{created_at := C1}, #{created_at := C2}) -> C1 > C2 end, + return({ok, emqx_mgmt_api:node_query(node(), Params, ?USERNAME_SCHEMA, ?query_username, SortFun)}). lookup_username(#{username := Username}, _Params) -> return({ok, format(emqx_auth_mnesia_cli:lookup_user({username, urldecode(Username)}))}). @@ -211,57 +228,52 @@ delete_username(#{username := Username}, _) -> %%------------------------------------------------------------------------------ %% Paging Query %%------------------------------------------------------------------------------ +query_clientid(Qs, Start, Limit) -> query(clientid, Qs, Start, Limit). +query_username(Qs, Start, Limit) -> query(username, Qs, Start, Limit). -paginate(Table, MatchSpec, Params, ComparingFun, RowFun) -> - Qh = query_handle(Table, MatchSpec), - Count = count(Table, MatchSpec), - paginate_qh(Qh, Count, Params, ComparingFun, RowFun). +query(Type, {Qs, []}, Start, Limit) -> + Ms = qs2ms(Type, Qs), + emqx_mgmt_api:select_table(?TABLE, Ms, Start, Limit, fun format/1); -paginate_qh(Qh, Count, Params, ComparingFun, RowFun) -> - Page = page(Params), - Limit = limit(Params), - Cursor = qlc:cursor(Qh), - case Page > 1 of - true -> - _ = qlc:next_answers(Cursor, (Page - 1) * Limit), - ok; - false -> ok - end, - Rows = qlc:next_answers(Cursor, Limit), - qlc:delete_cursor(Cursor), - #{meta => #{page => Page, limit => Limit, count => Count}, - data => [RowFun(Row) || Row <- lists:sort(ComparingFun, Rows)]}. +query(Type, {Qs, Fuzzy}, Start, Limit) -> + Ms = qs2ms(Type, Qs), + MatchFun = match_fun(Ms, Fuzzy), + emqx_mgmt_api:traverse_table(?TABLE, MatchFun, Start, Limit, fun format/1). -query_handle(Table, MatchSpec) when is_atom(Table) -> - Options = {traverse, {select, MatchSpec}}, - qlc:q([R || R <- ets:table(Table, Options)]). +-spec qs2ms(clientid | username, list()) -> ets:match_spec(). +qs2ms(Type, Qs) -> + Init = #?TABLE{login = {Type, '_'}, password = '_', created_at = '_'}, + MatchHead = lists:foldl(fun(Q, Acc) -> match_ms(Q, Acc) end, Init, Qs), + [{MatchHead, [], ['$_']}]. -count(Table, MatchSpec) when is_atom(Table) -> - [{MatchPattern, Where, _Re}] = MatchSpec, - NMatchSpec = [{MatchPattern, Where, [true]}], - ets:select_count(Table, NMatchSpec). +match_ms({Type, '=:=', Value}, MatchHead) -> MatchHead#?TABLE{login = {Type, Value}}; +match_ms(_, MatchHead) -> MatchHead. -page(Params) -> - binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)). - -limit(Params) -> - case proplists:get_value(<<"_limit">>, Params) of - undefined -> 10; - Size -> binary_to_integer(Size) +match_fun(Ms, Fuzzy) -> + MsC = ets:match_spec_compile(Ms), + fun(Rows) -> + Ls = ets:match_spec_run(Rows, MsC), + lists:filter(fun(E) -> run_fuzzy_match(E, Fuzzy) end, Ls) end. +run_fuzzy_match(_, []) -> true; +run_fuzzy_match(E = #?TABLE{login = {Key, Str}}, [{Key, like, SubStr}|Fuzzy]) -> + binary:match(Str, SubStr) /= nomatch andalso run_fuzzy_match(E, Fuzzy); +run_fuzzy_match(_E, [{_Key, like, _SubStr}| _Fuzzy]) -> false. + %%------------------------------------------------------------------------------ %% Interval Funcs %%------------------------------------------------------------------------------ -format([{?TABLE, {clientid, ClientId}, _Password, _InterTime}]) -> - #{clientid => ClientId}; +format([{?TABLE, {clientid, ClientId}, _Password, CreatedAt}]) -> + #{clientid => ClientId, created_at => CreatedAt}; -format([{?TABLE, {username, Username}, _Password, _InterTime}]) -> - #{username => Username}; +format([{?TABLE, {username, Username}, _Password, CreatedAt}]) -> + #{username => Username, created_at => CreatedAt}; format([]) -> - #{}. + #{}; +format(User) -> format([User]). validate([], []) -> ok; 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 e83dd346a..941ebedb9 100644 --- a/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl +++ b/apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl @@ -25,6 +25,7 @@ -include_lib("snabbkaffe/include/snabbkaffe.hrl"). -import(emqx_ct_http, [ request_api/3 + , request_api/4 , request_api/5 , get_http_data/1 , create_default_app/0 @@ -357,13 +358,31 @@ t_rest_api(_Config) -> <<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">> + }, + #{<<"clientid">> => <<"good_clientid1">>, + <<"topic">> => <<"topic/D">>, + <<"action">> => <<"pubsub">>, + <<"access">> => <<"deny">> }], {ok, _} = request_http_rest_add([], Params1), + {ok, Re1} = request_http_rest_list(["clientid", "test_clientid"]), ?assertMatch(4, length(get_http_data(Re1))), + {ok, Re11} = request_http_rest_list(["clientid"], "_like_clientid=good"), + ?assertMatch(2, length(get_http_data(Re11))), + {ok, Re12} = request_http_rest_list(["clientid"], "_like_clientid=clientid"), + ?assertMatch(6, length(get_http_data(Re12))), + {ok, Re13} = request_http_rest_list(["clientid"], "_like_clientid=clientid&action=pub"), + ?assertMatch(3, length(get_http_data(Re13))), + {ok, Re14} = request_http_rest_list(["clientid"], "_like_clientid=clientid&access=deny"), + ?assertMatch(4, length(get_http_data(Re14))), + {ok, Re15} = request_http_rest_list(["clientid"], "_like_clientid=clientid&topic=topic/A"), + ?assertMatch(1, length(get_http_data(Re15))), + {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, _} = request_http_rest_delete(["clientid", "good_clientid1", "topic", "topic/D"]), {ok, Res1} = request_http_rest_list(["clientid"]), ?assertMatch([], get_http_data(Res1)), @@ -381,13 +400,30 @@ t_rest_api(_Config) -> <<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">> + }, + #{<<"username">> => <<"good_username">>, + <<"topic">> => <<"topic/D">>, + <<"action">> => <<"pubsub">>, + <<"access">> => <<"deny">> }], {ok, _} = request_http_rest_add([], Params2), {ok, Re2} = request_http_rest_list(["username", "test_username"]), ?assertMatch(4, length(get_http_data(Re2))), + {ok, Re21} = request_http_rest_list(["username"], "_like_username=good"), + ?assertMatch(2, length(get_http_data(Re21))), + {ok, Re22} = request_http_rest_list(["username"], "_like_username=username"), + ?assertMatch(6, length(get_http_data(Re22))), + {ok, Re23} = request_http_rest_list(["username"], "_like_username=username&action=pub"), + ?assertMatch(3, length(get_http_data(Re23))), + {ok, Re24} = request_http_rest_list(["username"], "_like_username=username&access=deny"), + ?assertMatch(4, length(get_http_data(Re24))), + {ok, Re25} = request_http_rest_list(["username"], "_like_username=username&topic=topic/A"), + ?assertMatch(1, length(get_http_data(Re25))), + {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, _} = request_http_rest_delete(["username", "good_username", "topic", "topic/D"]), {ok, Res2} = request_http_rest_list(["username"]), ?assertMatch([], get_http_data(Res2)), @@ -402,13 +438,29 @@ t_rest_api(_Config) -> #{<<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">> - }], + }, + #{<<"topic">> => <<"topic/D">>, + <<"action">> => <<"pubsub">>, + <<"access">> => <<"deny">> + } + ], {ok, _} = request_http_rest_add([], Params3), + {ok, Re3} = request_http_rest_list(["$all"]), - ?assertMatch(4, length(get_http_data(Re3))), + ?assertMatch(6, length(get_http_data(Re3))), + {ok, Re31} = request_http_rest_list(["$all"], "topic=topic/A"), + ?assertMatch(1, length(get_http_data(Re31))), + {ok, Re32} = request_http_rest_list(["$all"], "action=sub"), + ?assertMatch(3, length(get_http_data(Re32))), + {ok, Re33} = request_http_rest_list(["$all"], "access=deny"), + ?assertMatch(4, length(get_http_data(Re33))), + {ok, Re34} = request_http_rest_list(["$all"], "action=sub&access=deny"), + ?assertMatch(2, length(get_http_data(Re34))), + {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, _} = request_http_rest_delete(["$all", "topic", "topic/D"]), {ok, Res3} = request_http_rest_list(["$all"]), ?assertMatch([], get_http_data(Res3)). @@ -442,6 +494,9 @@ combined_conflicting_records() -> request_http_rest_list(Path) -> request_api(get, uri(Path), default_auth_header()). +request_http_rest_list(Path, Qs) -> + request_api(get, uri(Path), Qs, default_auth_header()). + request_http_rest_lookup(Path) -> request_api(get, uri(Path), default_auth_header()). 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 26a2c1589..1dd979379 100644 --- a/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl +++ b/apps/emqx_auth_mnesia/test/emqx_auth_mnesia_SUITE.erl @@ -286,20 +286,26 @@ t_clientid_rest_api(_Config) -> Params3 = [ #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD} , #{<<"clientid">> => <<"clientid1">>, <<"password">> => ?PASSWORD} - , #{<<"clientid">> => <<"clientid2">>, <<"password">> => ?PASSWORD} + , #{<<"clientid">> => <<"client2">>, <<"password">> => ?PASSWORD} ], {ok, Result3} = request_http_rest_add(["auth_clientid"], Params3), ?assertMatch(#{ ?CLIENTID := <<"{error,existed}">> , <<"clientid1">> := <<"ok">> - , <<"clientid2">> := <<"ok">> + , <<"client2">> := <<"ok">> }, get_http_data(Result3)), {ok, Result4} = request_http_rest_list(["auth_clientid"]), + ?assertEqual(3, length(get_http_data(Result4))), + {ok, Result5} = request_http_rest_list(["auth_clientid?_like_clientid=id"]), + ?assertEqual(2, length(get_http_data(Result5))), + {ok, Result6} = request_http_rest_list(["auth_clientid?_like_clientid=x"]), + ?assertEqual(0, length(get_http_data(Result6))), + {ok, _} = request_http_rest_delete(Path), - {ok, Result5} = request_http_rest_lookup(Path), - ?assertMatch(#{}, get_http_data(Result5)). + {ok, Result7} = request_http_rest_lookup(Path), + ?assertMatch(#{}, get_http_data(Result7)). t_username_rest_api(_Config) -> clean_all_users(), @@ -330,9 +336,14 @@ t_username_rest_api(_Config) -> {ok, Result4} = request_http_rest_list(["auth_username"]), ?assertEqual(3, length(get_http_data(Result4))), + {ok, Result5} = request_http_rest_list(["auth_username?_like_username=for"]), + ?assertEqual(1, length(get_http_data(Result5))), + {ok, Result6} = request_http_rest_list(["auth_username?_like_username=x"]), + ?assertEqual(0, length(get_http_data(Result6))), + {ok, _} = request_http_rest_delete(Path), - {ok, Result5} = request_http_rest_lookup([Path]), - ?assertMatch(#{}, get_http_data(Result5)). + {ok, Result7} = request_http_rest_lookup([Path]), + ?assertMatch(#{}, get_http_data(Result7)). t_password_hash(_) -> clean_all_users(), diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index aeb17f733..c29f6bfdf 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -23,6 +23,7 @@ %% first_next query APIs -export([ params2qs/2 , node_query/4 + , node_query/5 , cluster_query/3 , traverse_table/5 , select_table/5 @@ -82,6 +83,9 @@ limit(Params) -> %%-------------------------------------------------------------------- node_query(Node, Params, {Tab, QsSchema}, QueryFun) -> + node_query(Node, Params, {Tab, QsSchema}, QueryFun, undefined). + +node_query(Node, Params, {Tab, QsSchema}, QueryFun, SortFun) -> {CodCnt, Qs} = params2qs(Params, QsSchema), Limit = limit(Params), Page = page(Params), @@ -94,7 +98,11 @@ node_query(Node, Params, {Tab, QsSchema}, QueryFun) -> true -> Meta#{count => count(Tab), hasnext => length(Rows) > Limit}; _ -> Meta#{count => -1, hasnext => length(Rows) > Limit} end, - #{meta => NMeta, data => lists:sublist(Rows, Limit)}. + NRows = case SortFun of + undefined -> Rows; + _ -> lists:sort(SortFun, Rows) + end, + #{meta => NMeta, data => lists:sublist(NRows, Limit)}. %% @private do_query(Node, Qs, {M,F}, Start, Limit) when Node =:= node() ->