Merge pull request #7969 from zhongwencool/auth-acl-fitler-support

feat: auth acl fitler support
This commit is contained in:
zhongwencool 2022-05-17 18:14:46 +08:00 committed by GitHub
commit 84fa6bfaeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 265 additions and 89 deletions

View File

@ -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] * 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 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. * Add proto_name and proto_ver fields for $event/client_disconnected event.
* Mnesia auth/acl http api support multiple condition queries.
### Bug fixes ### Bug fixes
* List subscription topic (/api/v4/subscriptions), the result do not match with multiple conditions. * List subscription topic (/api/v4/subscriptions), the result do not match with multiple conditions.

View File

@ -96,18 +96,24 @@
, delete/2 , 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) -> list_clientid(_Bindings, Params) ->
Table = emqx_acl_mnesia_db:login_acl_table(clientid), {_, Params1 = {_Qs, _Fuzzy}} = emqx_mgmt_api:params2qs(Params, ?CLIENTID_SCHEMA),
return({ok, emqx_auth_mnesia_api:paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). 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) -> list_username(_Bindings, Params) ->
Table = emqx_acl_mnesia_db:login_acl_table(username), {_, Params1 = {_Qs, _Fuzzy}} = emqx_mgmt_api:params2qs(Params, ?USERNAME_SCHEMA),
return({ok, emqx_auth_mnesia_api:paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). 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) -> list_all(_Bindings, Params) ->
Table = emqx_acl_mnesia_db:login_acl_table(all), {_, Params1 = {_Qs, _Fuzzy}} = emqx_mgmt_api:params2qs(Params, ?COMMON_SCHEMA),
return({ok, emqx_auth_mnesia_api:paginate_qh(Table, count(Table), Params, fun emqx_acl_mnesia_db:comparing/2, fun format/1)}). 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) -> lookup(#{clientid := Clientid}, _Params) ->
return({ok, format(emqx_acl_mnesia_db:lookup_acl({clientid, urldecode(Clientid)}))}); return({ok, format(emqx_acl_mnesia_db:lookup_acl({clientid, urldecode(Clientid)}))});
@ -170,7 +176,11 @@ delete(#{topic := Topic}, _) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
count(QH) -> 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}) -> format({{clientid, Clientid}, Topic, Action, Access, _CreatedAt}) ->
#{clientid => Clientid, topic => Topic, action => Action, access => Access}; #{clientid => Clientid, topic => Topic, action => Action, access => Access};
@ -222,3 +232,27 @@ format_msg(Message) when is_tuple(Message) ->
urldecode(S) -> urldecode(S) ->
emqx_http_lib:uri_decode(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.

View File

@ -33,6 +33,7 @@
, remove_acl/2 , remove_acl/2
, merge_acl_records/3 , merge_acl_records/3
, login_acl_table/1 , login_acl_table/1
, login_acl_table/2
, is_migration_started/0 , is_migration_started/0
]). ]).
@ -124,7 +125,7 @@ all_acls_export() ->
{atomic, Records} = mnesia:transaction( {atomic, Records} = mnesia:transaction(
fun() -> 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) qlc:eval(QH)
end), end),
Records. Records.
@ -132,9 +133,15 @@ all_acls_export() ->
%% @doc QLC table of logins matching spec %% @doc QLC table of logins matching spec
-spec(login_acl_table(acl_target_type()) -> qlc:query_handle()). -spec(login_acl_table(acl_target_type()) -> qlc:query_handle()).
login_acl_table(AclTargetType) -> login_acl_table(AclTargetType) ->
MatchSpecNew = login_match_spec_new(AclTargetType), login_acl_table(AclTargetType, {[], []}).
MatchSpecOld = login_match_spec_old(AclTargetType),
acl_table(MatchSpecNew, MatchSpecOld, fun ets:table/2, fun lookup_ets/2). 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 %% @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{}). -spec(merge_acl_records(acl_target(), [#?ACL_TABLE{}], [#?ACL_TABLE2{}]) -> #?ACL_TABLE2{}).
@ -223,27 +230,39 @@ comparing({_, _, _, _, CreatedAt1},
{_, _, _, _, CreatedAt2}) -> {_, _, _, _, CreatedAt2}) ->
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) -> ets:fun2ms(fun(#?ACL_TABLE{filter = {all, _}} = Record) ->
Record Record
end); end);
login_match_spec_old(Type) when (Type =:= username) or (Type =:= clientid) -> login_match_spec_old(Type, Params) when (Type =:= username) orelse (Type =:= clientid) ->
ets:fun2ms(fun(#?ACL_TABLE{filter = {{RecordType, _}, _}} = Record) case maps:get({Type, '=:='}, Params, undefined) of
when RecordType =:= Type -> Record undefined ->
end). 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) -> ets:fun2ms(fun(#?ACL_TABLE2{who = all} = Record) ->
Record Record
end); end);
login_match_spec_new(Type) when (Type =:= username) or (Type =:= clientid) -> login_match_spec_new(Type, Params) when (Type =:= username) orelse (Type =:= clientid) ->
ets:fun2ms(fun(#?ACL_TABLE2{who = {RecordType, _}} = Record) case maps:get({Type, '=:='}, Params, undefined) of
when RecordType =:= Type -> Record undefined ->
end). 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 = TraverseFun =
fun() -> fun() ->
CursorNew = CursorNew =
@ -252,7 +271,7 @@ acl_table(MatchSpecNew, MatchSpecOld, TableFun, LookupFun) ->
CursorOld = CursorOld =
qlc:cursor( qlc:cursor(
TableFun(?ACL_TABLE, [{traverse, {select, MatchSpecOld}}])), TableFun(?ACL_TABLE, [{traverse, {select, MatchSpecOld}}])),
traverse_new(CursorNew, CursorOld, #{}, LookupFun) traverse_new(CursorNew, CursorOld, Params, #{}, LookupFun)
end, end,
qlc:table(TraverseFun, []). 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 % After migration, number of such logins is zero, so traversing starts working in
% constant memory. % constant memory.
traverse_new(CursorNew, CursorOld, FoundKeys, LookupFun) -> traverse_new(CursorNew, CursorOld, Params, FoundKeys, LookupFun) ->
Acls = qlc:next_answers(CursorNew, 1), Acls = qlc:next_answers(CursorNew, 1),
case Acls of case Acls of
[] -> [] ->
qlc:delete_cursor(CursorNew), qlc:delete_cursor(CursorNew),
traverse_old(CursorOld, FoundKeys); traverse_old(CursorOld, Params, FoundKeys);
[#?ACL_TABLE2{who = Login, rules = Rules} = Acl] -> [#?ACL_TABLE2{who = Login, rules = Rules} = Acl] ->
Keys = lists:usort([{Login, Topic} || {_, _, Topic, _} <- Rules]), Keys = lists:usort([{Login, Topic} || {_, _, Topic, _} <- Rules]),
OldRecs = lists:flatmap(fun(Key) -> LookupFun(?ACL_TABLE, Key) end, Keys), OldRecs = lists:flatmap(fun(Key) -> LookupFun(?ACL_TABLE, Key) end, Keys),
@ -281,13 +300,41 @@ traverse_new(CursorNew, CursorOld, FoundKeys, LookupFun) ->
OldRecs), OldRecs),
case acl_to_list(MergedAcl) of case acl_to_list(MergedAcl) of
[] -> [] ->
traverse_new(CursorNew, CursorOld, NewFoundKeys, LookupFun); traverse_new(CursorNew, CursorOld, Params, NewFoundKeys, LookupFun);
List -> 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
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), OldAcls = qlc:next_answers(CursorOld),
case OldAcls of case OldAcls of
[] -> [] ->
@ -300,8 +347,10 @@ traverse_old(CursorOld, FoundKeys) ->
not maps:is_key({Login, Topic}, FoundKeys) not maps:is_key({Login, Topic}, FoundKeys)
], ],
case Records of case Records of
[] -> traverse_old(CursorOld, FoundKeys); [] -> traverse_old(CursorOld, Params, FoundKeys);
List -> List ++ fun() -> traverse_old(CursorOld, FoundKeys) end List ->
filter_params(List, Params)
++ fun() -> traverse_old(CursorOld, Params, FoundKeys) end
end end
end. end.

View File

@ -1,6 +1,6 @@
{application, emqx_auth_mnesia, {application, emqx_auth_mnesia,
[{description, "EMQ X Authentication with Mnesia"}, [{description, "EMQ X Authentication with Mnesia"},
{vsn, "4.3.6"}, % strict semver, bump manually {vsn, "4.3.7"}, % strict semver, bump manually
{modules, []}, {modules, []},
{registered, []}, {registered, []},
{applications, [kernel,stdlib,mnesia]}, {applications, [kernel,stdlib,mnesia]},

View File

@ -1,9 +1,10 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
%% Unless you know what you are doing, DO NOT edit manually!! %% Unless you know what you are doing, DO NOT edit manually!!
{VSN, {VSN,
[{"4.3.5", [{<<"4\\.3\\.[5-6]">>,
[{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, [{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]">>, {<<"4\\.3\\.[0-3]">>,
[{load_module,emqx_auth_mnesia_cli,brutal_purge,soft_purge,[]}, [{load_module,emqx_auth_mnesia_cli,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]},
@ -19,13 +20,16 @@
{"4.3.4", {"4.3.4",
[{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, [{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mnesia,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_auth_mnesia_cli,brutal_purge,soft_purge,[]},
{load_module,emqx_acl_mnesia,brutal_purge,soft_purge,[]}, {load_module,emqx_acl_mnesia,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mnesia_app,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_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]">>, {<<"4\\.3\\.[0-3]">>,
[{load_module,emqx_auth_mnesia_cli,brutal_purge,soft_purge,[]}, [{load_module,emqx_auth_mnesia_cli,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia,brutal_purge,soft_purge,[]},
@ -40,8 +44,10 @@
{delete_module,emqx_acl_mnesia_db}]}, {delete_module,emqx_acl_mnesia_db}]},
{"4.3.4", {"4.3.4",
[{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, [{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,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mnesia_cli,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_acl_mnesia,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mnesia_app,brutal_purge,soft_purge,[]}]}, {load_module,emqx_auth_mnesia_app,brutal_purge,soft_purge,[]}]},
{<<".*">>,[]}]}. {<<".*">>,[]}]}.

View File

@ -18,18 +18,20 @@
-include_lib("stdlib/include/qlc.hrl"). -include_lib("stdlib/include/qlc.hrl").
-include_lib("stdlib/include/ms_transform.hrl"). -include_lib("stdlib/include/ms_transform.hrl").
-include("emqx_auth_mnesia.hrl").
-define(TABLE, emqx_user). -define(TABLE, emqx_user).
-import(proplists, [get_value/2]). -import(proplists, [get_value/2]).
-import(minirest, [return/1]). -import(minirest, [return/1]).
-export([paginate_qh/5]).
-export([ list_clientid/2 -export([ list_clientid/2
, lookup_clientid/2 , lookup_clientid/2
, add_clientid/2 , add_clientid/2
, update_clientid/2 , update_clientid/2
, delete_clientid/2 , delete_clientid/2
, query_clientid/3
, query_username/3
]). ]).
-rest_api(#{name => list_clientid, -rest_api(#{name => list_clientid,
@ -109,13 +111,28 @@
descr => "Delete username in the cluster" 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 %% Auth Clientid Api
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
list_clientid(_Bindings, Params) -> list_clientid(_Bindings, Params) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) -> {?TABLE, {clientid, Clientid}, Password, CreatedAt} end), SortFun = fun(#{created_at := C1}, #{created_at := C2}) -> C1 > C2 end,
return({ok, paginate(?TABLE, MatchSpec, Params, fun emqx_auth_mnesia_cli:comparing/2, fun({?TABLE, {clientid, X}, _, _}) -> #{clientid => X} end)}). return({ok, emqx_mgmt_api:node_query(node(), Params, ?CLIENTID_SCHEMA, ?query_clientid, SortFun)}).
lookup_clientid(#{clientid := Clientid}, _Params) -> lookup_clientid(#{clientid := Clientid}, _Params) ->
return({ok, format(emqx_auth_mnesia_cli:lookup_user({clientid, urldecode(Clientid)}))}). return({ok, format(emqx_auth_mnesia_cli:lookup_user({clientid, urldecode(Clientid)}))}).
@ -164,8 +181,8 @@ delete_clientid(#{clientid := Clientid}, _) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
list_username(_Bindings, Params) -> list_username(_Bindings, Params) ->
MatchSpec = ets:fun2ms(fun({?TABLE, {username, Username}, Password, CreatedAt}) -> {?TABLE, {username, Username}, Password, CreatedAt} end), SortFun = fun(#{created_at := C1}, #{created_at := C2}) -> C1 > C2 end,
return({ok, paginate(?TABLE, MatchSpec, Params, fun emqx_auth_mnesia_cli:comparing/2, fun({?TABLE, {username, X}, _, _}) -> #{username => X} end)}). return({ok, emqx_mgmt_api:node_query(node(), Params, ?USERNAME_SCHEMA, ?query_username, SortFun)}).
lookup_username(#{username := Username}, _Params) -> lookup_username(#{username := Username}, _Params) ->
return({ok, format(emqx_auth_mnesia_cli:lookup_user({username, urldecode(Username)}))}). return({ok, format(emqx_auth_mnesia_cli:lookup_user({username, urldecode(Username)}))}).
@ -211,57 +228,52 @@ delete_username(#{username := Username}, _) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Paging Query %% 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) -> query(Type, {Qs, []}, Start, Limit) ->
Qh = query_handle(Table, MatchSpec), Ms = qs2ms(Type, Qs),
Count = count(Table, MatchSpec), emqx_mgmt_api:select_table(?TABLE, Ms, Start, Limit, fun format/1);
paginate_qh(Qh, Count, Params, ComparingFun, RowFun).
paginate_qh(Qh, Count, Params, ComparingFun, RowFun) -> query(Type, {Qs, Fuzzy}, Start, Limit) ->
Page = page(Params), Ms = qs2ms(Type, Qs),
Limit = limit(Params), MatchFun = match_fun(Ms, Fuzzy),
Cursor = qlc:cursor(Qh), emqx_mgmt_api:traverse_table(?TABLE, MatchFun, Start, Limit, fun format/1).
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_handle(Table, MatchSpec) when is_atom(Table) -> -spec qs2ms(clientid | username, list()) -> ets:match_spec().
Options = {traverse, {select, MatchSpec}}, qs2ms(Type, Qs) ->
qlc:q([R || R <- ets:table(Table, Options)]). 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) -> match_ms({Type, '=:=', Value}, MatchHead) -> MatchHead#?TABLE{login = {Type, Value}};
[{MatchPattern, Where, _Re}] = MatchSpec, match_ms(_, MatchHead) -> MatchHead.
NMatchSpec = [{MatchPattern, Where, [true]}],
ets:select_count(Table, NMatchSpec).
page(Params) -> match_fun(Ms, Fuzzy) ->
binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)). MsC = ets:match_spec_compile(Ms),
fun(Rows) ->
limit(Params) -> Ls = ets:match_spec_run(Rows, MsC),
case proplists:get_value(<<"_limit">>, Params) of lists:filter(fun(E) -> run_fuzzy_match(E, Fuzzy) end, Ls)
undefined -> 10;
Size -> binary_to_integer(Size)
end. 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 %% Interval Funcs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
format([{?TABLE, {clientid, ClientId}, _Password, _InterTime}]) -> format([{?TABLE, {clientid, ClientId}, _Password, CreatedAt}]) ->
#{clientid => ClientId}; #{clientid => ClientId, created_at => CreatedAt};
format([{?TABLE, {username, Username}, _Password, _InterTime}]) -> format([{?TABLE, {username, Username}, _Password, CreatedAt}]) ->
#{username => Username}; #{username => Username, created_at => CreatedAt};
format([]) -> format([]) ->
#{}. #{};
format(User) -> format([User]).
validate([], []) -> validate([], []) ->
ok; ok;

View File

@ -25,6 +25,7 @@
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-import(emqx_ct_http, [ request_api/3 -import(emqx_ct_http, [ request_api/3
, request_api/4
, request_api/5 , request_api/5
, get_http_data/1 , get_http_data/1
, create_default_app/0 , create_default_app/0
@ -357,13 +358,31 @@ t_rest_api(_Config) ->
<<"topic">> => <<"topic/C">>, <<"topic">> => <<"topic/C">>,
<<"action">> => <<"pubsub">>, <<"action">> => <<"pubsub">>,
<<"access">> => <<"deny">> <<"access">> => <<"deny">>
},
#{<<"clientid">> => <<"good_clientid1">>,
<<"topic">> => <<"topic/D">>,
<<"action">> => <<"pubsub">>,
<<"access">> => <<"deny">>
}], }],
{ok, _} = request_http_rest_add([], Params1), {ok, _} = request_http_rest_add([], Params1),
{ok, Re1} = request_http_rest_list(["clientid", "test_clientid"]), {ok, Re1} = request_http_rest_list(["clientid", "test_clientid"]),
?assertMatch(4, length(get_http_data(Re1))), ?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/A"]),
{ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/B"]), {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", "test_clientid", "topic", "topic/C"]),
{ok, _} = request_http_rest_delete(["clientid", "good_clientid1", "topic", "topic/D"]),
{ok, Res1} = request_http_rest_list(["clientid"]), {ok, Res1} = request_http_rest_list(["clientid"]),
?assertMatch([], get_http_data(Res1)), ?assertMatch([], get_http_data(Res1)),
@ -381,13 +400,30 @@ t_rest_api(_Config) ->
<<"topic">> => <<"topic/C">>, <<"topic">> => <<"topic/C">>,
<<"action">> => <<"pubsub">>, <<"action">> => <<"pubsub">>,
<<"access">> => <<"deny">> <<"access">> => <<"deny">>
},
#{<<"username">> => <<"good_username">>,
<<"topic">> => <<"topic/D">>,
<<"action">> => <<"pubsub">>,
<<"access">> => <<"deny">>
}], }],
{ok, _} = request_http_rest_add([], Params2), {ok, _} = request_http_rest_add([], Params2),
{ok, Re2} = request_http_rest_list(["username", "test_username"]), {ok, Re2} = request_http_rest_list(["username", "test_username"]),
?assertMatch(4, length(get_http_data(Re2))), ?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/A"]),
{ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/B"]), {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", "test_username", "topic", "topic/C"]),
{ok, _} = request_http_rest_delete(["username", "good_username", "topic", "topic/D"]),
{ok, Res2} = request_http_rest_list(["username"]), {ok, Res2} = request_http_rest_list(["username"]),
?assertMatch([], get_http_data(Res2)), ?assertMatch([], get_http_data(Res2)),
@ -402,13 +438,29 @@ t_rest_api(_Config) ->
#{<<"topic">> => <<"topic/C">>, #{<<"topic">> => <<"topic/C">>,
<<"action">> => <<"pubsub">>, <<"action">> => <<"pubsub">>,
<<"access">> => <<"deny">> <<"access">> => <<"deny">>
}], },
#{<<"topic">> => <<"topic/D">>,
<<"action">> => <<"pubsub">>,
<<"access">> => <<"deny">>
}
],
{ok, _} = request_http_rest_add([], Params3), {ok, _} = request_http_rest_add([], Params3),
{ok, Re3} = request_http_rest_list(["$all"]), {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/A"]),
{ok, _} = request_http_rest_delete(["$all", "topic", "topic/B"]), {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/C"]),
{ok, _} = request_http_rest_delete(["$all", "topic", "topic/D"]),
{ok, Res3} = request_http_rest_list(["$all"]), {ok, Res3} = request_http_rest_list(["$all"]),
?assertMatch([], get_http_data(Res3)). ?assertMatch([], get_http_data(Res3)).
@ -442,6 +494,9 @@ combined_conflicting_records() ->
request_http_rest_list(Path) -> request_http_rest_list(Path) ->
request_api(get, uri(Path), default_auth_header()). 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_http_rest_lookup(Path) ->
request_api(get, uri(Path), default_auth_header()). request_api(get, uri(Path), default_auth_header()).

View File

@ -286,20 +286,26 @@ t_clientid_rest_api(_Config) ->
Params3 = [ #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD} Params3 = [ #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD}
, #{<<"clientid">> => <<"clientid1">>, <<"password">> => ?PASSWORD} , #{<<"clientid">> => <<"clientid1">>, <<"password">> => ?PASSWORD}
, #{<<"clientid">> => <<"clientid2">>, <<"password">> => ?PASSWORD} , #{<<"clientid">> => <<"client2">>, <<"password">> => ?PASSWORD}
], ],
{ok, Result3} = request_http_rest_add(["auth_clientid"], Params3), {ok, Result3} = request_http_rest_add(["auth_clientid"], Params3),
?assertMatch(#{ ?CLIENTID := <<"{error,existed}">> ?assertMatch(#{ ?CLIENTID := <<"{error,existed}">>
, <<"clientid1">> := <<"ok">> , <<"clientid1">> := <<"ok">>
, <<"clientid2">> := <<"ok">> , <<"client2">> := <<"ok">>
}, get_http_data(Result3)), }, get_http_data(Result3)),
{ok, Result4} = request_http_rest_list(["auth_clientid"]), {ok, Result4} = request_http_rest_list(["auth_clientid"]),
?assertEqual(3, length(get_http_data(Result4))), ?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, _} = request_http_rest_delete(Path),
{ok, Result5} = request_http_rest_lookup(Path), {ok, Result7} = request_http_rest_lookup(Path),
?assertMatch(#{}, get_http_data(Result5)). ?assertMatch(#{}, get_http_data(Result7)).
t_username_rest_api(_Config) -> t_username_rest_api(_Config) ->
clean_all_users(), clean_all_users(),
@ -330,9 +336,14 @@ t_username_rest_api(_Config) ->
{ok, Result4} = request_http_rest_list(["auth_username"]), {ok, Result4} = request_http_rest_list(["auth_username"]),
?assertEqual(3, length(get_http_data(Result4))), ?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, _} = request_http_rest_delete(Path),
{ok, Result5} = request_http_rest_lookup([Path]), {ok, Result7} = request_http_rest_lookup([Path]),
?assertMatch(#{}, get_http_data(Result5)). ?assertMatch(#{}, get_http_data(Result7)).
t_password_hash(_) -> t_password_hash(_) ->
clean_all_users(), clean_all_users(),

View File

@ -23,6 +23,7 @@
%% first_next query APIs %% first_next query APIs
-export([ params2qs/2 -export([ params2qs/2
, node_query/4 , node_query/4
, node_query/5
, cluster_query/3 , cluster_query/3
, traverse_table/5 , traverse_table/5
, select_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) ->
node_query(Node, Params, {Tab, QsSchema}, QueryFun, undefined).
node_query(Node, Params, {Tab, QsSchema}, QueryFun, SortFun) ->
{CodCnt, Qs} = params2qs(Params, QsSchema), {CodCnt, Qs} = params2qs(Params, QsSchema),
Limit = limit(Params), Limit = limit(Params),
Page = page(Params), Page = page(Params),
@ -94,7 +98,11 @@ node_query(Node, Params, {Tab, QsSchema}, QueryFun) ->
true -> Meta#{count => count(Tab), hasnext => length(Rows) > Limit}; true -> Meta#{count => count(Tab), hasnext => length(Rows) > Limit};
_ -> Meta#{count => -1, hasnext => length(Rows) > Limit} _ -> Meta#{count => -1, hasnext => length(Rows) > Limit}
end, 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 %% @private
do_query(Node, Qs, {M,F}, Start, Limit) when Node =:= node() -> do_query(Node, Qs, {M,F}, Start, Limit) when Node =:= node() ->