Merge pull request #7225 from JimMoen/authn-authz-mnesia-fuzzy-searching

authn and authz mnesia searching by `clientid` or `username`
This commit is contained in:
JimMoen 2022-03-10 17:00:36 +08:00 committed by GitHub
commit 93c74bd645
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 401 additions and 212 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 ->

View File

@ -182,15 +182,22 @@ t_list_users(_) ->
fun(U) -> {ok, _} = emqx_authn_mnesia:add_user(U, State) end, fun(U) -> {ok, _} = emqx_authn_mnesia:add_user(U, State) end,
Users), Users),
{ok, #{data := [#{is_superuser := false,user_id := _},
#{data := [#{user_id := _}, #{user_id := _}], #{is_superuser := false,user_id := _}],
meta := #{page := 1, limit := 2, count := 3}}} = emqx_authn_mnesia:list_users( meta := #{page := 1, limit := 2, count := 3}} = emqx_authn_mnesia:list_users(
#{<<"page">> => 1, <<"limit">> => 2}, #{<<"page">> => 1, <<"limit">> => 2},
State), State),
{ok,
#{data := [#{user_id := _}], #{data := [#{is_superuser := false,user_id := _}],
meta := #{page := 2, limit := 2, count := 3}}} = emqx_authn_mnesia:list_users( meta := #{page := 2, limit := 2, count := 3}} = emqx_authn_mnesia:list_users(
#{<<"page">> => 2, <<"limit">> => 2}, #{<<"page">> => 2, <<"limit">> => 2},
State),
#{data := [#{is_superuser := false,user_id := <<"u3">>}],
meta := #{page := 1, limit := 20, count := 1}} = emqx_authn_mnesia:list_users(
#{ <<"page">> => 1
, <<"limit">> => 20
, <<"like_username">> => <<"3">>},
State). State).
t_import_users(_) -> t_import_users(_) ->

View File

@ -1,3 +1,19 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 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.
%%--------------------------------------------------------------------
-define(APP, emqx_authz). -define(APP, emqx_authz).
-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse
@ -8,6 +24,10 @@
(A =:= all) orelse (A =:= <<"all">>) (A =:= all) orelse (A =:= <<"all">>)
)). )).
%% authz_mnesia
-define(ACL_TABLE, emqx_acl).
%% authz_cmd
-define(CMD_REPLACE, replace). -define(CMD_REPLACE, replace).
-define(CMD_DELETE, delete). -define(CMD_DELETE, delete).
-define(CMD_PREPEND, prepend). -define(CMD_PREPEND, prepend).
@ -23,6 +43,7 @@
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9_]+\\}"). -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9_]+\\}").
%% API examples
-define(USERNAME_RULES_EXAMPLE, #{username => user1, -define(USERNAME_RULES_EXAMPLE, #{username => user1,
rules => [ #{topic => <<"test/toopic/1">>, rules => [ #{topic => <<"test/toopic/1">>,
permission => <<"allow">>, permission => <<"allow">>,

View File

@ -22,8 +22,14 @@
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-define(FORMAT_USERNAME_FUN, {?MODULE, format_by_username}). -import(hoconsc, [mk/1, mk/2, ref/1, ref/2, array/1, enum/1]).
-define(FORMAT_CLIENTID_FUN, {?MODULE, format_by_clientid}).
-define(QUERY_USERNAME_FUN, {?MODULE, query_username}).
-define(QUERY_CLIENTID_FUN, {?MODULE, query_clientid}).
-define(ACL_USERNAME_QSCHEMA, [{<<"like_username">>, binary}]).
-define(ACL_CLIENTID_QSCHEMA, [{<<"like_clientid">>, binary}]).
-export([ api_spec/0 -export([ api_spec/0
, paths/0 , paths/0
@ -40,8 +46,11 @@
, purge/2 , purge/2
]). ]).
-export([ format_by_username/1 %% query funs
, format_by_clientid/1]). -export([ query_username/4
, query_clientid/4]).
-export([format_result/1]).
-define(BAD_REQUEST, 'BAD_REQUEST'). -define(BAD_REQUEST, 'BAD_REQUEST').
-define(NOT_FOUND, 'NOT_FOUND'). -define(NOT_FOUND, 'NOT_FOUND').
@ -68,178 +77,191 @@ paths() ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
schema("/authorization/sources/built-in-database/username") -> schema("/authorization/sources/built-in-database/username") ->
#{ #{ 'operationId' => users
'operationId' => users, , get =>
get => #{ #{ tags => [<<"authorization">>]
tags => [<<"authorization">>], , description => <<"Show the list of record for username">>
description => <<"Show the list of record for username">>, , parameters =>
parameters => [ hoconsc:ref(emqx_dashboard_swagger, page) [ ref(emqx_dashboard_swagger, page)
, hoconsc:ref(emqx_dashboard_swagger, limit)], , ref(emqx_dashboard_swagger, limit)
responses => #{ , { like_username
200 => swagger_with_example( {username_response_data, ?TYPE_REF} , mk( binary(), #{ in => query
, required => false
, desc => <<"Fuzzy search `username` as substring">>})}
]
, responses =>
#{ 200 => swagger_with_example( {username_response_data, ?TYPE_REF}
, {username, ?PAGE_QUERY_EXAMPLE}) , {username, ?PAGE_QUERY_EXAMPLE})
} }
}, }
post => #{ , post =>
tags => [<<"authorization">>], #{ tags => [<<"authorization">>]
description => <<"Add new records for username">>, , description => <<"Add new records for username">>
'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_ARRAY} , 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_ARRAY}
, {username, ?POST_ARRAY_EXAMPLE}), , {username, ?POST_ARRAY_EXAMPLE})
responses => #{ , responses =>
204 => <<"Created">>, #{ 204 => <<"Created">>
400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST] , 400 => emqx_dashboard_swagger:error_codes(
, <<"Bad username or bad rule schema">>) [?BAD_REQUEST], <<"Bad username or bad rule schema">>)
} }
} }
}; };
schema("/authorization/sources/built-in-database/clientid") -> schema("/authorization/sources/built-in-database/clientid") ->
#{ #{ 'operationId' => clients
'operationId' => clients, , get =>
get => #{ #{ tags => [<<"authorization">>]
tags => [<<"authorization">>], , description => <<"Show the list of record for clientid">>
description => <<"Show the list of record for clientid">>, , parameters =>
parameters => [ hoconsc:ref(emqx_dashboard_swagger, page) [ ref(emqx_dashboard_swagger, page)
, hoconsc:ref(emqx_dashboard_swagger, limit)], , ref(emqx_dashboard_swagger, limit)
responses => #{ , { like_clientid
200 => swagger_with_example( {clientid_response_data, ?TYPE_REF} , mk( binary()
, #{ in => query
, required => false
, desc => <<"Fuzzy search `clientid` as substring">>})
}
]
, responses =>
#{ 200 => swagger_with_example( {clientid_response_data, ?TYPE_REF}
, {clientid, ?PAGE_QUERY_EXAMPLE}) , {clientid, ?PAGE_QUERY_EXAMPLE})
} }
}, }
post => #{ , post =>
tags => [<<"authorization">>], #{ tags => [<<"authorization">>]
description => <<"Add new records for clientid">>, , description => <<"Add new records for clientid">>
'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_ARRAY} , 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_ARRAY}
, {clientid, ?POST_ARRAY_EXAMPLE}), , {clientid, ?POST_ARRAY_EXAMPLE})
responses => #{ , responses =>
204 => <<"Created">>, #{ 204 => <<"Created">>
400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST] , 400 => emqx_dashboard_swagger:error_codes(
, <<"Bad clientid or bad rule schema">>) [?BAD_REQUEST], <<"Bad clientid or bad rule schema">>)
} }
} }
}; };
schema("/authorization/sources/built-in-database/username/:username") -> schema("/authorization/sources/built-in-database/username/:username") ->
#{ #{ 'operationId' => user
'operationId' => user, , get =>
get => #{ #{ tags => [<<"authorization">>]
tags => [<<"authorization">>], , description => <<"Get record info for username">>
description => <<"Get record info for username">>, , parameters => [ref(username)]
parameters => [hoconsc:ref(username)], , responses =>
responses => #{ #{ 200 => swagger_with_example( {rules_for_username, ?TYPE_REF}
200 => swagger_with_example( {rules_for_username, ?TYPE_REF} , {username, ?PUT_MAP_EXAMPLE})
, {username, ?PUT_MAP_EXAMPLE}), , 404 => emqx_dashboard_swagger:error_codes(
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>) [?NOT_FOUND], <<"Not Found">>)
} }
},
put => #{
tags => [<<"authorization">>],
description => <<"Set record for username">>,
parameters => [hoconsc:ref(username)],
'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_REF}
, {username, ?PUT_MAP_EXAMPLE}),
responses => #{
204 => <<"Updated">>,
400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST]
, <<"Bad username or bad rule schema">>)
} }
}, , put =>
delete => #{ #{ tags => [<<"authorization">>]
tags => [<<"authorization">>], , description => <<"Set record for username">>
description => <<"Delete one record for username">>, , parameters => [ref(username)]
parameters => [hoconsc:ref(username)], , 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_REF}
responses => #{ , {username, ?PUT_MAP_EXAMPLE})
204 => <<"Deleted">>, , responses =>
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad username">>) #{ 204 => <<"Updated">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad username or bad rule schema">>)
}
}
, delete =>
#{ tags => [<<"authorization">>]
, description => <<"Delete one record for username">>
, parameters => [ref(username)]
, responses =>
#{ 204 => <<"Deleted">>
, 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad username">>)
} }
} }
}; };
schema("/authorization/sources/built-in-database/clientid/:clientid") -> schema("/authorization/sources/built-in-database/clientid/:clientid") ->
#{ #{ 'operationId' => client
'operationId' => client, , get =>
get => #{ #{ tags => [<<"authorization">>]
tags => [<<"authorization">>], , description => <<"Get record info for clientid">>
description => <<"Get record info for clientid">>, , parameters => [ref(clientid)]
parameters => [hoconsc:ref(clientid)], , responses =>
responses => #{ #{ 200 => swagger_with_example( {rules_for_clientid, ?TYPE_REF}
200 => swagger_with_example( {rules_for_clientid, ?TYPE_REF} , {clientid, ?PUT_MAP_EXAMPLE})
, {clientid, ?PUT_MAP_EXAMPLE}), , 404 => emqx_dashboard_swagger:error_codes(
404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>) [?NOT_FOUND], <<"Not Found">>)
} }
}, },
put => #{ put =>
tags => [<<"authorization">>], #{ tags => [<<"authorization">>]
description => <<"Set record for clientid">>, , description => <<"Set record for clientid">>
parameters => [hoconsc:ref(clientid)], , parameters => [ref(clientid)]
'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_REF} , 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_REF}
, {clientid, ?PUT_MAP_EXAMPLE}), , {clientid, ?PUT_MAP_EXAMPLE})
responses => #{ , responses =>
204 => <<"Updated">>, #{ 204 => <<"Updated">>
400 => emqx_dashboard_swagger:error_codes( , 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid or bad rule schema">>) [?BAD_REQUEST], <<"Bad clientid or bad rule schema">>)
} }
}, }
delete => #{ , delete =>
tags => [<<"authorization">>], #{ tags => [<<"authorization">>]
description => <<"Delete one record for clientid">>, , description => <<"Delete one record for clientid">>
parameters => [hoconsc:ref(clientid)], , parameters => [ref(clientid)]
responses => #{ , responses =>
204 => <<"Deleted">>, #{ 204 => <<"Deleted">>
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad clientid">>) , 400 => emqx_dashboard_swagger:error_codes(
[?BAD_REQUEST], <<"Bad clientid">>)
} }
} }
}; };
schema("/authorization/sources/built-in-database/all") -> schema("/authorization/sources/built-in-database/all") ->
#{ #{ 'operationId' => all
'operationId' => all, , get =>
get => #{ #{ tags => [<<"authorization">>]
tags => [<<"authorization">>], , description => <<"Show the list of rules for all">>
description => <<"Show the list of rules for all">>, , responses =>
responses => #{ #{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})}
200 => swagger_with_example({rules_for_all, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})
} }
}, , put =>
put => #{ #{ tags => [<<"authorization">>]
tags => [<<"authorization">>], , description => <<"Set the list of rules for all">>
description => <<"Set the list of rules for all">>, , 'requestBody' =>
'requestBody' => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})
swagger_with_example({rules_for_all, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}), , responses =>
responses => #{ #{ 204 => <<"Created">>
204 => <<"Created">>, , 400 => emqx_dashboard_swagger:error_codes(
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad rule schema">>) [?BAD_REQUEST], <<"Bad rule schema">>)
} }
} }
}; };
schema("/authorization/sources/built-in-database/purge-all") -> schema("/authorization/sources/built-in-database/purge-all") ->
#{ #{ 'operationId' => purge
'operationId' => purge, , delete =>
delete => #{ #{ tags => [<<"authorization">>]
tags => [<<"authorization">>], , description => <<"Purge all records">>
description => <<"Purge all records">>, , responses =>
responses => #{ #{ 204 => <<"Deleted">>
204 => <<"Deleted">>, , 400 => emqx_dashboard_swagger:error_codes(
400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>) [?BAD_REQUEST], <<"Bad Request">>)
} }
} }
}. }.
fields(rule_item) -> fields(rule_item) ->
[ {topic, hoconsc:mk(string(), [ {topic, mk(string(),
#{ required => true #{ required => true
, desc => <<"Rule on specific topic">> , desc => <<"Rule on specific topic">>
, example => <<"test/topic/1">> , example => <<"test/topic/1">>
})} })}
, {permission, hoconsc:mk(hoconsc:enum([allow, deny]), , {permission, mk(enum([allow, deny]),
#{ desc => <<"Permission">> #{ desc => <<"Permission">>
, required => true , required => true
, example => allow , example => allow
})} })}
, {action, hoconsc:mk(hoconsc:enum([publish, subscribe, all]), , {action, mk(enum([publish, subscribe, all]),
#{ required => true #{ required => true
, example => publish , example => publish
, desc => <<"Authorized action">> , desc => <<"Authorized action">>
})} })}
]; ];
fields(clientid) -> fields(clientid) ->
[ {clientid, hoconsc:mk(binary(), [ {clientid, mk(binary(),
#{ in => path #{ in => path
, required => true , required => true
, desc => <<"ClientID">> , desc => <<"ClientID">>
@ -247,50 +269,51 @@ fields(clientid) ->
})} })}
]; ];
fields(username) -> fields(username) ->
[ {username, hoconsc:mk(binary(), [ {username, mk(binary(),
#{ in => path #{ in => path
, required => true , required => true
, desc => <<"Username">> , desc => <<"Username">>
, example => <<"user1">>})} , example => <<"user1">>})}
]; ];
fields(rules_for_username) -> fields(rules_for_username) ->
[ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})} fields(rules)
] ++ fields(username); ++ fields(username);
fields(username_response_data) -> fields(username_response_data) ->
[ {data, hoconsc:mk(hoconsc:array(hoconsc:ref(rules_for_username)), #{})} [ {data, mk(array(ref(rules_for_username)), #{})}
, {meta, hoconsc:ref(meta)} , {meta, ref(meta)}
]; ];
fields(rules_for_clientid) -> fields(rules_for_clientid) ->
[ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})} fields(rules)
] ++ fields(clientid); ++ fields(clientid);
fields(clientid_response_data) -> fields(clientid_response_data) ->
[ {data, hoconsc:mk(hoconsc:array(hoconsc:ref(rules_for_clientid)), #{})} [ {data, mk(array(ref(rules_for_clientid)), #{})}
, {meta, hoconsc:ref(meta)} , {meta, ref(meta)}
];
fields(rules_for_all) ->
[ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})}
]; ];
fields(rules) ->
[{rules, mk(array(ref(rule_item)))}];
fields(meta) -> fields(meta) ->
emqx_dashboard_swagger:fields(page) emqx_dashboard_swagger:fields(page)
++ emqx_dashboard_swagger:fields(limit) ++ emqx_dashboard_swagger:fields(limit)
++ [{count, hoconsc:mk(integer(), #{example => 1})}]. ++ [{count, mk(integer(), #{example => 1})}].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% HTTP API %% HTTP API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
users(get, #{query_string := PageParams}) -> users(get, #{query_string := QueryString}) ->
{Table, MatchSpec} = emqx_authz_mnesia:list_username_rules(), Response = emqx_mgmt_api:node_query(node(), QueryString,
{200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?FORMAT_USERNAME_FUN)}; ?ACL_TABLE, ?ACL_USERNAME_QSCHEMA, ?QUERY_USERNAME_FUN),
emqx_mgmt_util:generate_response(Response);
users(post, #{body := Body}) when is_list(Body) -> users(post, #{body := Body}) when is_list(Body) ->
lists:foreach(fun(#{<<"username">> := Username, <<"rules">> := Rules}) -> lists:foreach(fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)) emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules))
end, Body), end, Body),
{204}. {204}.
clients(get, #{query_string := PageParams}) -> clients(get, #{query_string := QueryString}) ->
{Table, MatchSpec} = emqx_authz_mnesia:list_clientid_rules(), Response = emqx_mgmt_api:node_query(node(), QueryString,
{200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?FORMAT_CLIENTID_FUN)}; ?ACL_TABLE, ?ACL_CLIENTID_QSCHEMA, ?QUERY_CLIENTID_FUN),
emqx_mgmt_util:generate_response(Response);
clients(post, #{body := Body}) when is_list(Body) -> clients(post, #{body := Body}) when is_list(Body) ->
lists:foreach(fun(#{<<"clientid">> := Clientid, <<"rules">> := Rules}) -> lists:foreach(fun(#{<<"clientid">> := Clientid, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({clientid, Clientid}, format_rules(Rules)) emqx_authz_mnesia:store_rules({clientid, Clientid}, format_rules(Rules))
@ -365,6 +388,54 @@ purge(delete, _) ->
}} }}
end. end.
%%--------------------------------------------------------------------
%% Query Functions
query_username(Tab, {_QString, []}, Continuation, Limit) ->
Ms = emqx_authz_mnesia:list_username_rules(),
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
fun format_result/1);
query_username(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
Ms = emqx_authz_mnesia:list_username_rules(),
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
fun format_result/1).
query_clientid(Tab, {_QString, []}, Continuation, Limit) ->
Ms = emqx_authz_mnesia:list_clientid_rules(),
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
fun format_result/1);
query_clientid(Tab, {_QString, FuzzyQString}, Continuation, Limit) ->
Ms = emqx_authz_mnesia:list_clientid_rules(),
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
fun format_result/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 = [{username, Username}, _Rule]
, [{username, like, UsernameSubStr} | Fuzzy]) ->
binary:match(Username, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy);
run_fuzzy_filter( E = [{clientid, ClientId}, _Rule]
, [{clientid, like, ClientIdSubStr} | Fuzzy]) ->
binary:match(ClientId, ClientIdSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
%%--------------------------------------------------------------------
%% format funcs
%% format rule from api
format_rules(Rules) when is_list(Rules) -> format_rules(Rules) when is_list(Rules) ->
lists:foldl(fun(#{<<"topic">> := Topic, lists:foldl(fun(#{<<"topic">> := Topic,
<<"action">> := Action, <<"action">> := Action,
@ -374,14 +445,15 @@ format_rules(Rules) when is_list(Rules) ->
AccIn ++ [{ atom(Permission), atom(Action), Topic }] AccIn ++ [{ atom(Permission), atom(Action), Topic }]
end, [], Rules). end, [], Rules).
format_by_username([{username, Username}, {rules, Rules}]) -> %% format result from mnesia tab
format_result([{username, Username}, {rules, Rules}]) ->
#{username => Username, #{username => Username,
rules => [ #{topic => Topic, rules => [ #{topic => Topic,
action => Action, action => Action,
permission => Permission permission => Permission
} || {Permission, Action, Topic} <- Rules] } || {Permission, Action, Topic} <- Rules]
}. };
format_by_clientid([{clientid, Clientid}, {rules, Rules}]) -> format_result([{clientid, Clientid}, {rules, Rules}]) ->
#{clientid => Clientid, #{clientid => Clientid,
rules => [ #{topic => Topic, rules => [ #{topic => Topic,
action => Action, action => Action,
@ -402,8 +474,8 @@ atom(A) when is_atom(A) -> A.
swagger_with_example({Ref, TypeP}, {_Name, _Type} = Example) -> swagger_with_example({Ref, TypeP}, {_Name, _Type} = Example) ->
emqx_dashboard_swagger:schema_with_examples( emqx_dashboard_swagger:schema_with_examples(
case TypeP of case TypeP of
?TYPE_REF -> hoconsc:ref(?MODULE, Ref); ?TYPE_REF -> ref(?MODULE, Ref);
?TYPE_ARRAY -> hoconsc:array(hoconsc:ref(?MODULE, Ref)) ?TYPE_ARRAY -> array(ref(?MODULE, Ref))
end, end,
rules_example(Example)). rules_example(Example)).

View File

@ -20,9 +20,9 @@
-include_lib("stdlib/include/ms_transform.hrl"). -include_lib("stdlib/include/ms_transform.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-define(ACL_SHARDED, emqx_acl_sharded). -include("emqx_authz.hrl").
-define(ACL_TABLE, emqx_acl). -define(ACL_SHARDED, emqx_acl_sharded).
%% To save some space, use an integer for label, 0 for 'all', {1, Username} and {2, ClientId}. %% To save some space, use an integer for label, 0 for 'all', {1, Username} and {2, ClientId}.
-define(ACL_TABLE_ALL, 0). -define(ACL_TABLE_ALL, 0).
@ -114,10 +114,12 @@ authorize(#{username := Username,
%% Management API %% Management API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Init
-spec(init_tables() -> ok). -spec(init_tables() -> ok).
init_tables() -> init_tables() ->
ok = mria_rlog:wait_for_shards([?ACL_SHARDED], infinity). ok = mria_rlog:wait_for_shards([?ACL_SHARDED], infinity).
%% @doc Update authz rules
-spec(store_rules(who(), rules()) -> ok). -spec(store_rules(who(), rules()) -> ok).
store_rules({username, Username}, Rules) -> store_rules({username, Username}, Rules) ->
Record = #emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = normalize_rules(Rules)}, Record = #emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = normalize_rules(Rules)},
@ -129,6 +131,7 @@ store_rules(all, Rules) ->
Record = #emqx_acl{who = ?ACL_TABLE_ALL, rules = normalize_rules(Rules)}, Record = #emqx_acl{who = ?ACL_TABLE_ALL, rules = normalize_rules(Rules)},
mria:dirty_write(Record). mria:dirty_write(Record).
%% @doc Clean all authz rules for (username & clientid & all)
-spec(purge_rules() -> ok). -spec(purge_rules() -> ok).
purge_rules() -> purge_rules() ->
ok = lists:foreach( ok = lists:foreach(
@ -137,6 +140,7 @@ purge_rules() ->
end, end,
mnesia:dirty_all_keys(?ACL_TABLE)). mnesia:dirty_all_keys(?ACL_TABLE)).
%% @doc Get one record
-spec(get_rules(who()) -> {ok, rules()} | not_found). -spec(get_rules(who()) -> {ok, rules()} | not_found).
get_rules({username, Username}) -> get_rules({username, Username}) ->
do_get_rules({?ACL_TABLE_USERNAME, Username}); do_get_rules({?ACL_TABLE_USERNAME, Username});
@ -145,6 +149,7 @@ get_rules({clientid, Clientid}) ->
get_rules(all) -> get_rules(all) ->
do_get_rules(?ACL_TABLE_ALL). do_get_rules(?ACL_TABLE_ALL).
%% @doc Delete one record
-spec(delete_rules(who()) -> ok). -spec(delete_rules(who()) -> ok).
delete_rules({username, Username}) -> delete_rules({username, Username}) ->
mria:dirty_delete(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username}); mria:dirty_delete(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username});
@ -153,21 +158,19 @@ delete_rules({clientid, Clientid}) ->
delete_rules(all) -> delete_rules(all) ->
mria:dirty_delete(?ACL_TABLE, ?ACL_TABLE_ALL). mria:dirty_delete(?ACL_TABLE, ?ACL_TABLE_ALL).
-spec(list_username_rules() -> {mria:table(), ets:match_spec()}). -spec(list_username_rules() -> ets:match_spec()).
list_username_rules() -> list_username_rules() ->
MatchSpec = ets:fun2ms( ets:fun2ms(
fun(#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}) -> fun(#emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = Rules}) ->
[{username, Username}, {rules, Rules}] [{username, Username}, {rules, Rules}]
end), end).
{?ACL_TABLE, MatchSpec}.
-spec(list_clientid_rules() -> {mria:table(), ets:match_spec()}). -spec(list_clientid_rules() -> ets:match_spec()).
list_clientid_rules() -> list_clientid_rules() ->
MatchSpec = ets:fun2ms( ets:fun2ms(
fun(#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}) -> fun(#emqx_acl{who = {?ACL_TABLE_CLIENTID, Clientid}, rules = Rules}) ->
[{clientid, Clientid}, {rules, Rules}] [{clientid, Clientid}, {rules, Rules}]
end), end).
{?ACL_TABLE, MatchSpec}.
-spec(record_count() -> non_neg_integer()). -spec(record_count() -> non_neg_integer()).
record_count() -> record_count() ->

View File

@ -76,21 +76,37 @@ t_api(_) ->
request( post request( post
, uri(["authorization", "sources", "built-in-database", "username"]) , uri(["authorization", "sources", "built-in-database", "username"])
, [?USERNAME_RULES_EXAMPLE]), , [?USERNAME_RULES_EXAMPLE]),
{ok, 200, Request1} = {ok, 200, Request1} =
request( get request( get
, uri(["authorization", "sources", "built-in-database", "username"]) , uri(["authorization", "sources", "built-in-database", "username"])
, []), , []),
{ok, 200, Request2} =
request( get
, uri(["authorization", "sources", "built-in-database", "username", "user1"])
, []),
#{<<"data">> := [#{<<"username">> := <<"user1">>, <<"rules">> := Rules1}], #{<<"data">> := [#{<<"username">> := <<"user1">>, <<"rules">> := Rules1}],
<<"meta">> := #{<<"count">> := 1, <<"meta">> := #{<<"count">> := 1,
<<"limit">> := 100, <<"limit">> := 100,
<<"page">> := 1}} = jsx:decode(Request1), <<"page">> := 1}} = jsx:decode(Request1),
#{<<"username">> := <<"user1">>, <<"rules">> := Rules1} = jsx:decode(Request2),
?assertEqual(3, length(Rules1)), ?assertEqual(3, length(Rules1)),
{ok, 200, Request1_1} =
request( get
, uri([ "authorization"
, "sources"
, "built-in-database"
, "username?page=1&limit=20&like_username=noexist"])
, []),
#{<<"data">> := [],
<<"meta">> := #{<<"count">> := 0,
<<"limit">> := 20,
<<"page">> := 1}} = jsx:decode(Request1_1),
{ok, 200, Request2} =
request( get
, uri(["authorization", "sources", "built-in-database", "username", "user1"])
, []),
#{<<"username">> := <<"user1">>, <<"rules">> := Rules1} = jsx:decode(Request2),
{ok, 204, _} = {ok, 204, _} =
request( put request( put
, uri(["authorization", "sources", "built-in-database", "username", "user1"]) , uri(["authorization", "sources", "built-in-database", "username", "user1"])

View File

@ -99,7 +99,7 @@ authn(delete, #{bindings := #{name := Name0}}) ->
users(get, #{bindings := #{name := Name0}, query_string := Qs}) -> users(get, #{bindings := #{name := Name0}, query_string := Qs}) ->
with_authn(Name0, fun(_GwName, #{id := AuthId, with_authn(Name0, fun(_GwName, #{id := AuthId,
chain_name := ChainName}) -> chain_name := ChainName}) ->
emqx_authn_api:list_users(ChainName, AuthId, page_pramas(Qs)) emqx_authn_api:list_users(ChainName, AuthId, parse_qstring(Qs))
end); end);
users(post, #{bindings := #{name := Name0}, users(post, #{bindings := #{name := Name0},
body := Body}) -> body := Body}) ->
@ -145,8 +145,11 @@ import_users(post, #{bindings := #{name := Name0},
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Utils %% Utils
page_pramas(Qs) -> parse_qstring(Qs) ->
maps:with([<<"page">>, <<"limit">>], Qs). maps:with([ <<"page">>
, <<"limit">>
, <<"like_username">>
, <<"like_clientid">>], Qs).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Swagger defines %% Swagger defines
@ -190,7 +193,8 @@ schema("/gateway/:name/authentication/users") ->
, get => , get =>
#{ description => <<"Get the users for the authentication">> #{ description => <<"Get the users for the authentication">>
, parameters => params_gateway_name_in_path() ++ , parameters => params_gateway_name_in_path() ++
params_paging_in_qs() params_paging_in_qs() ++
params_fuzzy_in_qs()
, responses => , responses =>
?STANDARD_RESP( ?STANDARD_RESP(
#{ 200 => emqx_dashboard_swagger:schema_with_example( #{ 200 => emqx_dashboard_swagger:schema_with_example(
@ -299,6 +303,23 @@ params_paging_in_qs() ->
})} })}
]. ].
params_fuzzy_in_qs() ->
[{like_username,
mk(binary(),
#{ in => query
, required => false
, desc => <<"Fuzzy search by username">>
, example => <<"username">>
})},
{like_clientid,
mk(binary(),
#{ in => query
, required => false
, desc => <<"Fuzzy search by clientid">>
, example => <<"clientid">>
})}
].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% schemas %% schemas