feat(authn): authn mnesia rows fuzzy searching by `clientid` or `username`

This commit is contained in:
JimMoen 2022-03-07 11:27:10 +08:00
parent 16ec8fe289
commit 593e1a3efb
3 changed files with 67 additions and 18 deletions

View File

@ -378,8 +378,8 @@ lookup_user(ChainName, AuthenticatorID, UserID) ->
call({lookup_user, ChainName, AuthenticatorID, UserID}). call({lookup_user, ChainName, AuthenticatorID, UserID}).
-spec list_users(chain_name(), authenticator_id(), map()) -> {ok, [user_info()]} | {error, term()}. -spec list_users(chain_name(), authenticator_id(), map()) -> {ok, [user_info()]} | {error, term()}.
list_users(ChainName, AuthenticatorID, Params) -> list_users(ChainName, AuthenticatorID, FuzzyParams) ->
call({list_users, ChainName, AuthenticatorID, Params}). call({list_users, ChainName, AuthenticatorID, FuzzyParams}).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks
@ -476,8 +476,8 @@ handle_call({lookup_user, ChainName, AuthenticatorID, UserID}, _From, State) ->
Reply = call_authenticator(ChainName, AuthenticatorID, lookup_user, [UserID]), Reply = call_authenticator(ChainName, AuthenticatorID, lookup_user, [UserID]),
reply(Reply, State); reply(Reply, State);
handle_call({list_users, ChainName, AuthenticatorID, PageParams}, _From, State) -> handle_call({list_users, ChainName, AuthenticatorID, FuzzyParams}, _From, State) ->
Reply = call_authenticator(ChainName, AuthenticatorID, list_users, [PageParams]), Reply = call_authenticator(ChainName, AuthenticatorID, list_users, [FuzzyParams]),
reply(Reply, State); reply(Reply, State);
handle_call(Req, _From, State) -> handle_call(Req, _From, State) ->

View File

@ -380,7 +380,13 @@ schema("/authentication/:id/users") ->
parameters => [ parameters => [
param_auth_id(), param_auth_id(),
{page, mk(integer(), #{in => query, desc => <<"Page Index">>, required => false})}, {page, mk(integer(), #{in => query, desc => <<"Page Index">>, required => false})},
{limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, required => false})} {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, required => false})},
{like_username, mk(binary(), #{ in => query
, desc => <<"Fuzzy search username">>
, required => false})},
{like_clientid, mk(binary(), #{ in => query
, desc => <<"Fuzzy search clientid">>
, required => false})}
], ],
responses => #{ responses => #{
200 => emqx_dashboard_swagger:schema_with_example( 200 => emqx_dashboard_swagger:schema_with_example(
@ -638,8 +644,8 @@ listener_authenticator_import_users(post, #{bindings := #{listener_id := _, id :
authenticator_users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> authenticator_users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) ->
add_user(?GLOBAL, AuthenticatorID, UserInfo); add_user(?GLOBAL, AuthenticatorID, UserInfo);
authenticator_users(get, #{bindings := #{id := AuthenticatorID}, query_string := PageParams}) -> authenticator_users(get, #{bindings := #{id := AuthenticatorID}, query_string := QueryString}) ->
list_users(?GLOBAL, AuthenticatorID, PageParams). list_users(?GLOBAL, AuthenticatorID, QueryString).
authenticator_user(put, #{bindings := #{id := AuthenticatorID, authenticator_user(put, #{bindings := #{id := AuthenticatorID,
user_id := UserID}, body := UserInfo}) -> user_id := UserID}, body := UserInfo}) ->
@ -840,13 +846,9 @@ delete_user(ChainName, AuthenticatorID, UserID) ->
serialize_error({user_error, Reason}) serialize_error({user_error, Reason})
end. end.
list_users(ChainName, AuthenticatorID, PageParams) -> list_users(ChainName, AuthenticatorID, QueryString) ->
case emqx_authentication:list_users(ChainName, AuthenticatorID, PageParams) of Response = emqx_authentication:list_users(ChainName, AuthenticatorID, QueryString),
{ok, Users} -> emqx_mgmt_util:generate_response(Response).
{200, Users};
{error, Reason} ->
serialize_error(Reason)
end.
update_config(Path, ConfigRequest) -> update_config(Path, ConfigRequest) ->
emqx_conf:update(Path, ConfigRequest, #{rawconf_with_defaults => true, emqx_conf:update(Path, ConfigRequest, #{rawconf_with_defaults => true,

View File

@ -43,7 +43,9 @@
, list_users/2 , list_users/2
]). ]).
-export([format_user_info/1]). -export([ query/4
, format_user_info/1
, group_match_spec/1]).
-type user_id_type() :: clientid | username. -type user_id_type() :: clientid | username.
-type user_group() :: binary(). -type user_group() :: binary().
@ -63,7 +65,10 @@
-boot_mnesia({mnesia, [boot]}). -boot_mnesia({mnesia, [boot]}).
-define(TAB, ?MODULE). -define(TAB, ?MODULE).
-define(FORMAT_FUN, {?MODULE, format_user_info}). -define(AUTHN_QSCHEMA, [ {<<"like_username">>, binary}
, {<<"like_clientid">>, binary}
, {<<"user_group">>, binary}]).
-define(QUERY_FUN, {?MODULE, query}).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Mnesia bootstrap %% Mnesia bootstrap
@ -219,8 +224,42 @@ lookup_user(UserID, #{user_group := UserGroup}) ->
{error, not_found} {error, not_found}
end. end.
list_users(PageParams, #{user_group := UserGroup}) -> list_users(QueryString, #{user_group := UserGroup}) ->
{ok, emqx_mgmt_api:paginate(?TAB, group_match_spec(UserGroup), PageParams, ?FORMAT_FUN)}. NQueryString = QueryString#{<<"user_group">> => UserGroup},
emqx_mgmt_api:node_query(node(), NQueryString, ?TAB, ?AUTHN_QSCHEMA, ?QUERY_FUN).
%%--------------------------------------------------------------------
%% Query Functions
query(Tab, {QString, []}, Continuation, Limit) ->
Ms = ms_from_qstring(QString),
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
fun format_user_info/1);
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
Ms = ms_from_qstring(QString),
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
fun format_user_info/1).
%%--------------------------------------------------------------------
%% Match funcs
%% Fuzzy username funcs
fuzzy_filter_fun(Fuzzy) ->
fun(MsRaws) when is_list(MsRaws) ->
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
, MsRaws)
end.
run_fuzzy_filter(_, []) ->
true;
run_fuzzy_filter( E = #user_info{user_id = {_, UserID}}
, [{username, like, UsernameSubStr} | Fuzzy]) ->
binary:match(UserID, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy);
run_fuzzy_filter( E = #user_info{user_id = {_, UserID}}
, [{clientid, like, ClientIDSubStr} | Fuzzy]) ->
binary:match(UserID, ClientIDSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Internal functions %% Internal functions
@ -352,6 +391,14 @@ to_binary(L) when is_list(L) ->
format_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) -> format_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) ->
#{user_id => UserID, is_superuser => IsSuperuser}. #{user_id => UserID, is_superuser => IsSuperuser}.
ms_from_qstring(QString) ->
[Ms] = lists:foldl(fun({user_group, '=:=', UserGroup}, AccIn) ->
[group_match_spec(UserGroup) | AccIn];
(_, AccIn) ->
AccIn
end, [], QString),
Ms.
group_match_spec(UserGroup) -> group_match_spec(UserGroup) ->
ets:fun2ms( ets:fun2ms(
fun(#user_info{user_id = {Group, _}} = User) when Group =:= UserGroup -> fun(#user_info{user_id = {Group, _}} = User) when Group =:= UserGroup ->