From 593e1a3efbbf4297ca4600677ff4076e39a45827 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Mon, 7 Mar 2022 11:27:10 +0800 Subject: [PATCH] feat(authn): authn mnesia rows fuzzy searching by `clientid` or `username` --- apps/emqx/src/emqx_authentication.erl | 8 +-- apps/emqx_authn/src/emqx_authn_api.erl | 22 ++++---- .../src/simple_authn/emqx_authn_mnesia.erl | 55 +++++++++++++++++-- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl index 18798598e..6dc4c4bbd 100644 --- a/apps/emqx/src/emqx_authentication.erl +++ b/apps/emqx/src/emqx_authentication.erl @@ -378,8 +378,8 @@ lookup_user(ChainName, AuthenticatorID, UserID) -> call({lookup_user, ChainName, AuthenticatorID, UserID}). -spec list_users(chain_name(), authenticator_id(), map()) -> {ok, [user_info()]} | {error, term()}. -list_users(ChainName, AuthenticatorID, Params) -> - call({list_users, ChainName, AuthenticatorID, Params}). +list_users(ChainName, AuthenticatorID, FuzzyParams) -> + call({list_users, ChainName, AuthenticatorID, FuzzyParams}). %%-------------------------------------------------------------------- %% 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(Reply, State); -handle_call({list_users, ChainName, AuthenticatorID, PageParams}, _From, State) -> - Reply = call_authenticator(ChainName, AuthenticatorID, list_users, [PageParams]), +handle_call({list_users, ChainName, AuthenticatorID, FuzzyParams}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, list_users, [FuzzyParams]), reply(Reply, State); handle_call(Req, _From, State) -> diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index af4c3da2d..623012bee 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -380,7 +380,13 @@ schema("/authentication/:id/users") -> parameters => [ param_auth_id(), {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 => #{ 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}) -> add_user(?GLOBAL, AuthenticatorID, UserInfo); -authenticator_users(get, #{bindings := #{id := AuthenticatorID}, query_string := PageParams}) -> - list_users(?GLOBAL, AuthenticatorID, PageParams). +authenticator_users(get, #{bindings := #{id := AuthenticatorID}, query_string := QueryString}) -> + list_users(?GLOBAL, AuthenticatorID, QueryString). authenticator_user(put, #{bindings := #{id := AuthenticatorID, user_id := UserID}, body := UserInfo}) -> @@ -840,13 +846,9 @@ delete_user(ChainName, AuthenticatorID, UserID) -> serialize_error({user_error, Reason}) end. -list_users(ChainName, AuthenticatorID, PageParams) -> - case emqx_authentication:list_users(ChainName, AuthenticatorID, PageParams) of - {ok, Users} -> - {200, Users}; - {error, Reason} -> - serialize_error(Reason) - end. +list_users(ChainName, AuthenticatorID, QueryString) -> + Response = emqx_authentication:list_users(ChainName, AuthenticatorID, QueryString), + emqx_mgmt_util:generate_response(Response). update_config(Path, ConfigRequest) -> emqx_conf:update(Path, ConfigRequest, #{rawconf_with_defaults => true, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index b21521d79..7ff9e85ef 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -43,7 +43,9 @@ , 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_group() :: binary(). @@ -63,7 +65,10 @@ -boot_mnesia({mnesia, [boot]}). -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 @@ -219,8 +224,42 @@ lookup_user(UserID, #{user_group := UserGroup}) -> {error, not_found} end. -list_users(PageParams, #{user_group := UserGroup}) -> - {ok, emqx_mgmt_api:paginate(?TAB, group_match_spec(UserGroup), PageParams, ?FORMAT_FUN)}. +list_users(QueryString, #{user_group := UserGroup}) -> + 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 @@ -352,6 +391,14 @@ to_binary(L) when is_list(L) -> format_user_info(#user_info{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) -> ets:fun2ms( fun(#user_info{user_id = {Group, _}} = User) when Group =:= UserGroup ->