style(authn): reformat authn subdir source files
This commit is contained in:
parent
8500200e81
commit
3022ee081d
|
@ -23,47 +23,54 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
-behaviour(emqx_authentication).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ refs/0
|
-export([
|
||||||
, create/2
|
refs/0,
|
||||||
, update/2
|
create/2,
|
||||||
, authenticate/2
|
update/2,
|
||||||
, destroy/1
|
authenticate/2,
|
||||||
]).
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ add_user/2
|
-export([
|
||||||
, delete_user/2
|
add_user/2,
|
||||||
, update_user/3
|
delete_user/2,
|
||||||
, lookup_user/2
|
update_user/3,
|
||||||
, list_users/2
|
lookup_user/2,
|
||||||
]).
|
list_users/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ query/4
|
-export([
|
||||||
, format_user_info/1
|
query/4,
|
||||||
, group_match_spec/1]).
|
format_user_info/1,
|
||||||
|
group_match_spec/1
|
||||||
|
]).
|
||||||
|
|
||||||
-define(TAB, ?MODULE).
|
-define(TAB, ?MODULE).
|
||||||
-define(AUTHN_QSCHEMA, [ {<<"like_username">>, binary}
|
-define(AUTHN_QSCHEMA, [
|
||||||
, {<<"user_group">>, binary}]).
|
{<<"like_username">>, binary},
|
||||||
|
{<<"user_group">>, binary}
|
||||||
|
]).
|
||||||
-define(QUERY_FUN, {?MODULE, query}).
|
-define(QUERY_FUN, {?MODULE, query}).
|
||||||
|
|
||||||
-type(user_group() :: binary()).
|
-type user_group() :: binary().
|
||||||
|
|
||||||
-export([mnesia/1]).
|
-export([mnesia/1]).
|
||||||
|
|
||||||
-boot_mnesia({mnesia, [boot]}).
|
-boot_mnesia({mnesia, [boot]}).
|
||||||
|
|
||||||
-record(user_info,
|
-record(user_info, {
|
||||||
{ user_id
|
user_id,
|
||||||
, stored_key
|
stored_key,
|
||||||
, server_key
|
server_key,
|
||||||
, salt
|
salt,
|
||||||
, is_superuser
|
is_superuser
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-reflect_type([user_group/0]).
|
-reflect_type([user_group/0]).
|
||||||
|
|
||||||
|
@ -72,14 +79,15 @@
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
%% @doc Create or replicate tables.
|
%% @doc Create or replicate tables.
|
||||||
-spec(mnesia(boot | copy) -> ok).
|
-spec mnesia(boot | copy) -> ok.
|
||||||
mnesia(boot) ->
|
mnesia(boot) ->
|
||||||
ok = mria:create_table(?TAB, [
|
ok = mria:create_table(?TAB, [
|
||||||
{rlog_shard, ?AUTH_SHARD},
|
{rlog_shard, ?AUTH_SHARD},
|
||||||
{storage, disc_copies},
|
{storage, disc_copies},
|
||||||
{record_name, user_info},
|
{record_name, user_info},
|
||||||
{attributes, record_info(fields, user_info)},
|
{attributes, record_info(fields, user_info)},
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]).
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Hocon Schema
|
%% Hocon Schema
|
||||||
|
@ -90,10 +98,11 @@ namespace() -> "authn-scram-builtin_db".
|
||||||
roots() -> [?CONF_NS].
|
roots() -> [?CONF_NS].
|
||||||
|
|
||||||
fields(?CONF_NS) ->
|
fields(?CONF_NS) ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('scram')}
|
[
|
||||||
, {backend, emqx_authn_schema:backend('built_in_database')}
|
{mechanism, emqx_authn_schema:mechanism('scram')},
|
||||||
, {algorithm, fun algorithm/1}
|
{backend, emqx_authn_schema:backend('built_in_database')},
|
||||||
, {iteration_count, fun iteration_count/1}
|
{algorithm, fun algorithm/1},
|
||||||
|
{iteration_count, fun iteration_count/1}
|
||||||
] ++ emqx_authn_schema:common_fields().
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
algorithm(type) -> hoconsc:enum([sha256, sha512]);
|
algorithm(type) -> hoconsc:enum([sha256, sha512]);
|
||||||
|
@ -109,23 +118,33 @@ iteration_count(_) -> undefined.
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
[hoconsc:ref(?MODULE, ?CONF_NS)].
|
[hoconsc:ref(?MODULE, ?CONF_NS)].
|
||||||
|
|
||||||
create(AuthenticatorID,
|
create(
|
||||||
#{algorithm := Algorithm,
|
AuthenticatorID,
|
||||||
iteration_count := IterationCount}) ->
|
#{
|
||||||
State = #{user_group => AuthenticatorID,
|
algorithm := Algorithm,
|
||||||
algorithm => Algorithm,
|
iteration_count := IterationCount
|
||||||
iteration_count => IterationCount},
|
}
|
||||||
|
) ->
|
||||||
|
State = #{
|
||||||
|
user_group => AuthenticatorID,
|
||||||
|
algorithm => Algorithm,
|
||||||
|
iteration_count => IterationCount
|
||||||
|
},
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
|
|
||||||
|
|
||||||
update(Config, #{user_group := ID}) ->
|
update(Config, #{user_group := ID}) ->
|
||||||
create(ID, Config).
|
create(ID, Config).
|
||||||
|
|
||||||
authenticate(#{auth_method := AuthMethod,
|
authenticate(
|
||||||
auth_data := AuthData,
|
#{
|
||||||
auth_cache := AuthCache}, State) ->
|
auth_method := AuthMethod,
|
||||||
|
auth_data := AuthData,
|
||||||
|
auth_cache := AuthCache
|
||||||
|
},
|
||||||
|
State
|
||||||
|
) ->
|
||||||
case ensure_auth_method(AuthMethod, State) of
|
case ensure_auth_method(AuthMethod, State) of
|
||||||
true ->
|
true ->
|
||||||
case AuthCache of
|
case AuthCache of
|
||||||
|
@ -144,13 +163,22 @@ destroy(#{user_group := UserGroup}) ->
|
||||||
MatchSpec = group_match_spec(UserGroup),
|
MatchSpec = group_match_spec(UserGroup),
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
ok = lists:foreach(fun(UserInfo) ->
|
ok = lists:foreach(
|
||||||
mnesia:delete_object(?TAB, UserInfo, write)
|
fun(UserInfo) ->
|
||||||
end, mnesia:select(?TAB, MatchSpec, write))
|
mnesia:delete_object(?TAB, UserInfo, write)
|
||||||
end).
|
end,
|
||||||
|
mnesia:select(?TAB, MatchSpec, write)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
add_user(#{user_id := UserID,
|
add_user(
|
||||||
password := Password} = UserInfo, #{user_group := UserGroup} = State) ->
|
#{
|
||||||
|
user_id := UserID,
|
||||||
|
password := Password
|
||||||
|
} = UserInfo,
|
||||||
|
#{user_group := UserGroup} = State
|
||||||
|
) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
|
@ -161,7 +189,8 @@ add_user(#{user_id := UserID,
|
||||||
[_] ->
|
[_] ->
|
||||||
{error, already_exist}
|
{error, already_exist}
|
||||||
end
|
end
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
delete_user(UserID, #{user_group := UserGroup}) ->
|
delete_user(UserID, #{user_group := UserGroup}) ->
|
||||||
trans(
|
trans(
|
||||||
|
@ -172,30 +201,42 @@ delete_user(UserID, #{user_group := UserGroup}) ->
|
||||||
[_] ->
|
[_] ->
|
||||||
mnesia:delete(?TAB, {UserGroup, UserID}, write)
|
mnesia:delete(?TAB, {UserGroup, UserID}, write)
|
||||||
end
|
end
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
update_user(UserID, User,
|
update_user(
|
||||||
#{user_group := UserGroup} = State) ->
|
UserID,
|
||||||
|
User,
|
||||||
|
#{user_group := UserGroup} = State
|
||||||
|
) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
[] ->
|
[] ->
|
||||||
{error, not_found};
|
{error, not_found};
|
||||||
[#user_info{is_superuser = IsSuperuser} = UserInfo] ->
|
[#user_info{is_superuser = IsSuperuser} = UserInfo] ->
|
||||||
UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, IsSuperuser)},
|
UserInfo1 = UserInfo#user_info{
|
||||||
UserInfo2 = case maps:get(password, User, undefined) of
|
is_superuser = maps:get(is_superuser, User, IsSuperuser)
|
||||||
undefined ->
|
},
|
||||||
UserInfo1;
|
UserInfo2 =
|
||||||
Password ->
|
case maps:get(password, User, undefined) of
|
||||||
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
|
undefined ->
|
||||||
UserInfo1#user_info{stored_key = StoredKey,
|
UserInfo1;
|
||||||
server_key = ServerKey,
|
Password ->
|
||||||
salt = Salt}
|
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(
|
||||||
end,
|
Password, State
|
||||||
|
),
|
||||||
|
UserInfo1#user_info{
|
||||||
|
stored_key = StoredKey,
|
||||||
|
server_key = ServerKey,
|
||||||
|
salt = Salt
|
||||||
|
}
|
||||||
|
end,
|
||||||
mnesia:write(?TAB, UserInfo2, write),
|
mnesia:write(?TAB, UserInfo2, write),
|
||||||
{ok, format_user_info(UserInfo2)}
|
{ok, format_user_info(UserInfo2)}
|
||||||
end
|
end
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
lookup_user(UserID, #{user_group := UserGroup}) ->
|
lookup_user(UserID, #{user_group := UserGroup}) ->
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
|
@ -214,14 +255,23 @@ list_users(QueryString, #{user_group := UserGroup}) ->
|
||||||
|
|
||||||
query(Tab, {QString, []}, Continuation, Limit) ->
|
query(Tab, {QString, []}, Continuation, Limit) ->
|
||||||
Ms = ms_from_qstring(QString),
|
Ms = ms_from_qstring(QString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_user_info/1);
|
Tab,
|
||||||
|
Ms,
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_user_info/1
|
||||||
|
);
|
||||||
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
Ms = ms_from_qstring(QString),
|
Ms = ms_from_qstring(QString),
|
||||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_user_info/1).
|
Tab,
|
||||||
|
{Ms, FuzzyFilterFun},
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_user_info/1
|
||||||
|
).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Match funcs
|
%% Match funcs
|
||||||
|
@ -229,14 +279,18 @@ query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
%% Fuzzy username funcs
|
%% Fuzzy username funcs
|
||||||
fuzzy_filter_fun(Fuzzy) ->
|
fuzzy_filter_fun(Fuzzy) ->
|
||||||
fun(MsRaws) when is_list(MsRaws) ->
|
fun(MsRaws) when is_list(MsRaws) ->
|
||||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
lists:filter(
|
||||||
, MsRaws)
|
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||||
|
MsRaws
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run_fuzzy_filter(_, []) ->
|
run_fuzzy_filter(_, []) ->
|
||||||
true;
|
true;
|
||||||
run_fuzzy_filter( E = #user_info{user_id = {_, UserID}}
|
run_fuzzy_filter(
|
||||||
, [{username, like, UsernameSubStr} | Fuzzy]) ->
|
E = #user_info{user_id = {_, UserID}},
|
||||||
|
[{username, like, UsernameSubStr} | Fuzzy]
|
||||||
|
) ->
|
||||||
binary:match(UserID, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
binary:match(UserID, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -252,13 +306,17 @@ ensure_auth_method(_, _) ->
|
||||||
|
|
||||||
check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) ->
|
check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) ->
|
||||||
RetrieveFun = fun(Username) ->
|
RetrieveFun = fun(Username) ->
|
||||||
retrieve(Username, State)
|
retrieve(Username, State)
|
||||||
end,
|
end,
|
||||||
case esasl_scram:check_client_first_message(
|
case
|
||||||
Bin,
|
esasl_scram:check_client_first_message(
|
||||||
#{iteration_count => IterationCount,
|
Bin,
|
||||||
retrieve => RetrieveFun}
|
#{
|
||||||
) of
|
iteration_count => IterationCount,
|
||||||
|
retrieve => RetrieveFun
|
||||||
|
}
|
||||||
|
)
|
||||||
|
of
|
||||||
{continue, ServerFirstMessage, Cache} ->
|
{continue, ServerFirstMessage, Cache} ->
|
||||||
{continue, ServerFirstMessage, Cache};
|
{continue, ServerFirstMessage, Cache};
|
||||||
ignore ->
|
ignore ->
|
||||||
|
@ -268,10 +326,12 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}) ->
|
check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}) ->
|
||||||
case esasl_scram:check_client_final_message(
|
case
|
||||||
Bin,
|
esasl_scram:check_client_final_message(
|
||||||
Cache#{algorithm => Alg}
|
Bin,
|
||||||
) of
|
Cache#{algorithm => Alg}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, ServerFinalMessage} ->
|
{ok, ServerFinalMessage} ->
|
||||||
{ok, #{is_superuser => IsSuperuser}, ServerFinalMessage};
|
{ok, #{is_superuser => IsSuperuser}, ServerFinalMessage};
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
|
@ -280,23 +340,31 @@ check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algori
|
||||||
|
|
||||||
add_user(UserGroup, UserID, Password, IsSuperuser, State) ->
|
add_user(UserGroup, UserID, Password, IsSuperuser, State) ->
|
||||||
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
|
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
|
||||||
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
UserInfo = #user_info{
|
||||||
stored_key = StoredKey,
|
user_id = {UserGroup, UserID},
|
||||||
server_key = ServerKey,
|
stored_key = StoredKey,
|
||||||
salt = Salt,
|
server_key = ServerKey,
|
||||||
is_superuser = IsSuperuser},
|
salt = Salt,
|
||||||
|
is_superuser = IsSuperuser
|
||||||
|
},
|
||||||
mnesia:write(?TAB, UserInfo, write).
|
mnesia:write(?TAB, UserInfo, write).
|
||||||
|
|
||||||
retrieve(UserID, #{user_group := UserGroup}) ->
|
retrieve(UserID, #{user_group := UserGroup}) ->
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
[#user_info{stored_key = StoredKey,
|
[
|
||||||
server_key = ServerKey,
|
#user_info{
|
||||||
salt = Salt,
|
stored_key = StoredKey,
|
||||||
is_superuser = IsSuperuser}] ->
|
server_key = ServerKey,
|
||||||
{ok, #{stored_key => StoredKey,
|
salt = Salt,
|
||||||
server_key => ServerKey,
|
is_superuser = IsSuperuser
|
||||||
salt => Salt,
|
}
|
||||||
is_superuser => IsSuperuser}};
|
] ->
|
||||||
|
{ok, #{
|
||||||
|
stored_key => StoredKey,
|
||||||
|
server_key => ServerKey,
|
||||||
|
salt => Salt,
|
||||||
|
is_superuser => IsSuperuser
|
||||||
|
}};
|
||||||
[] ->
|
[] ->
|
||||||
{error, not_found}
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
|
@ -315,15 +383,21 @@ 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_from_qstring(QString) ->
|
||||||
[Ms] = lists:foldl(fun({user_group, '=:=', UserGroup}, AccIn) ->
|
[Ms] = lists:foldl(
|
||||||
[group_match_spec(UserGroup) | AccIn];
|
fun
|
||||||
(_, AccIn) ->
|
({user_group, '=:=', UserGroup}, AccIn) ->
|
||||||
AccIn
|
[group_match_spec(UserGroup) | AccIn];
|
||||||
end, [], QString),
|
(_, AccIn) ->
|
||||||
|
AccIn
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
QString
|
||||||
|
),
|
||||||
Ms.
|
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 ->
|
||||||
User
|
User
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
|
@ -18,9 +18,10 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
-export([ introduced_in/0
|
-export([
|
||||||
, lookup_from_all_nodes/3
|
introduced_in/0,
|
||||||
]).
|
lookup_from_all_nodes/3
|
||||||
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/bpapi.hrl").
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
||||||
|
@ -30,6 +31,8 @@ introduced_in() ->
|
||||||
"5.0.0".
|
"5.0.0".
|
||||||
|
|
||||||
-spec lookup_from_all_nodes([node()], atom(), binary()) ->
|
-spec lookup_from_all_nodes([node()], atom(), binary()) ->
|
||||||
emqx_rpc:erpc_multicall().
|
emqx_rpc:erpc_multicall().
|
||||||
lookup_from_all_nodes(Nodes, ChainName, AuthenticatorID) ->
|
lookup_from_all_nodes(Nodes, ChainName, AuthenticatorID) ->
|
||||||
erpc:multicall(Nodes, emqx_authn_api, lookup_from_local_node, [ChainName, AuthenticatorID], ?TIMEOUT).
|
erpc:multicall(
|
||||||
|
Nodes, emqx_authn_api, lookup_from_local_node, [ChainName, AuthenticatorID], ?TIMEOUT
|
||||||
|
).
|
||||||
|
|
|
@ -24,18 +24,20 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
-behaviour(emqx_authentication).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
, validations/0
|
fields/1,
|
||||||
]).
|
validations/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ refs/0
|
-export([
|
||||||
, create/2
|
refs/0,
|
||||||
, update/2
|
create/2,
|
||||||
, authenticate/2
|
update/2,
|
||||||
, destroy/1
|
authenticate/2,
|
||||||
]).
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Hocon Schema
|
%% Hocon Schema
|
||||||
|
@ -44,35 +46,47 @@
|
||||||
namespace() -> "authn-http".
|
namespace() -> "authn-http".
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[ {?CONF_NS,
|
[
|
||||||
hoconsc:mk(hoconsc:union(refs()),
|
{?CONF_NS,
|
||||||
#{})}
|
hoconsc:mk(
|
||||||
|
hoconsc:union(refs()),
|
||||||
|
#{}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
fields(get) ->
|
fields(get) ->
|
||||||
[ {method, #{type => get, default => post}}
|
[
|
||||||
, {headers, fun headers_no_content_type/1}
|
{method, #{type => get, default => post}},
|
||||||
|
{headers, fun headers_no_content_type/1}
|
||||||
] ++ common_fields();
|
] ++ common_fields();
|
||||||
|
|
||||||
fields(post) ->
|
fields(post) ->
|
||||||
[ {method, #{type => post, default => post}}
|
[
|
||||||
, {headers, fun headers/1}
|
{method, #{type => post, default => post}},
|
||||||
|
{headers, fun headers/1}
|
||||||
] ++ common_fields().
|
] ++ common_fields().
|
||||||
|
|
||||||
common_fields() ->
|
common_fields() ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
[
|
||||||
, {backend, emqx_authn_schema:backend(http)}
|
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||||
, {url, fun url/1}
|
{backend, emqx_authn_schema:backend(http)},
|
||||||
, {body, map([{fuzzy, term(), binary()}])}
|
{url, fun url/1},
|
||||||
, {request_timeout, fun request_timeout/1}
|
{body, map([{fuzzy, term(), binary()}])},
|
||||||
] ++ emqx_authn_schema:common_fields()
|
{request_timeout, fun request_timeout/1}
|
||||||
++ maps:to_list(maps:without([ base_url
|
] ++ emqx_authn_schema:common_fields() ++
|
||||||
, pool_type],
|
maps:to_list(
|
||||||
maps:from_list(emqx_connector_http:fields(config)))).
|
maps:without(
|
||||||
|
[
|
||||||
|
base_url,
|
||||||
|
pool_type
|
||||||
|
],
|
||||||
|
maps:from_list(emqx_connector_http:fields(config))
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
validations() ->
|
validations() ->
|
||||||
[ {check_ssl_opts, fun check_ssl_opts/1}
|
[
|
||||||
, {check_headers, fun check_headers/1}
|
{check_ssl_opts, fun check_ssl_opts/1},
|
||||||
|
{check_headers, fun check_headers/1}
|
||||||
].
|
].
|
||||||
|
|
||||||
url(type) -> binary();
|
url(type) -> binary();
|
||||||
|
@ -80,21 +94,27 @@ url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")];
|
||||||
url(required) -> true;
|
url(required) -> true;
|
||||||
url(_) -> undefined.
|
url(_) -> undefined.
|
||||||
|
|
||||||
headers(type) -> map();
|
headers(type) ->
|
||||||
|
map();
|
||||||
headers(converter) ->
|
headers(converter) ->
|
||||||
fun(Headers) ->
|
fun(Headers) ->
|
||||||
maps:merge(default_headers(), transform_header_name(Headers))
|
maps:merge(default_headers(), transform_header_name(Headers))
|
||||||
end;
|
end;
|
||||||
headers(default) -> default_headers();
|
headers(default) ->
|
||||||
headers(_) -> undefined.
|
default_headers();
|
||||||
|
headers(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
headers_no_content_type(type) -> map();
|
headers_no_content_type(type) ->
|
||||||
|
map();
|
||||||
headers_no_content_type(converter) ->
|
headers_no_content_type(converter) ->
|
||||||
fun(Headers) ->
|
fun(Headers) ->
|
||||||
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
||||||
end;
|
end;
|
||||||
headers_no_content_type(default) -> default_headers_no_content_type();
|
headers_no_content_type(default) ->
|
||||||
headers_no_content_type(_) -> undefined.
|
default_headers_no_content_type();
|
||||||
|
headers_no_content_type(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
request_timeout(type) -> emqx_schema:duration_ms();
|
request_timeout(type) -> emqx_schema:duration_ms();
|
||||||
request_timeout(default) -> <<"5s">>;
|
request_timeout(default) -> <<"5s">>;
|
||||||
|
@ -105,36 +125,51 @@ request_timeout(_) -> undefined.
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
[ hoconsc:ref(?MODULE, get)
|
[
|
||||||
, hoconsc:ref(?MODULE, post)
|
hoconsc:ref(?MODULE, get),
|
||||||
|
hoconsc:ref(?MODULE, post)
|
||||||
].
|
].
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
create(#{method := Method,
|
create(
|
||||||
url := RawURL,
|
#{
|
||||||
headers := Headers,
|
method := Method,
|
||||||
body := Body,
|
url := RawURL,
|
||||||
request_timeout := RequestTimeout} = Config) ->
|
headers := Headers,
|
||||||
|
body := Body,
|
||||||
|
request_timeout := RequestTimeout
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
{BsaeUrlWithPath, Query} = parse_fullpath(RawURL),
|
{BsaeUrlWithPath, Query} = parse_fullpath(RawURL),
|
||||||
URIMap = parse_url(BsaeUrlWithPath),
|
URIMap = parse_url(BsaeUrlWithPath),
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
State = #{method => Method,
|
State = #{
|
||||||
path => maps:get(path, URIMap),
|
method => Method,
|
||||||
base_query_template => emqx_authn_utils:parse_deep(
|
path => maps:get(path, URIMap),
|
||||||
cow_qs:parse_qs(to_bin(Query))),
|
base_query_template => emqx_authn_utils:parse_deep(
|
||||||
headers => maps:to_list(Headers),
|
cow_qs:parse_qs(to_bin(Query))
|
||||||
body_template => emqx_authn_utils:parse_deep(
|
),
|
||||||
maps:to_list(Body)),
|
headers => maps:to_list(Headers),
|
||||||
request_timeout => RequestTimeout,
|
body_template => emqx_authn_utils:parse_deep(
|
||||||
resource_id => ResourceId},
|
maps:to_list(Body)
|
||||||
case emqx_resource:create_local(ResourceId,
|
),
|
||||||
?RESOURCE_GROUP,
|
request_timeout => RequestTimeout,
|
||||||
emqx_connector_http,
|
resource_id => ResourceId
|
||||||
Config#{base_url => maps:remove(query, URIMap),
|
},
|
||||||
pool_type => random},
|
case
|
||||||
#{}) of
|
emqx_resource:create_local(
|
||||||
|
ResourceId,
|
||||||
|
?RESOURCE_GROUP,
|
||||||
|
emqx_connector_http,
|
||||||
|
Config#{
|
||||||
|
base_url => maps:remove(query, URIMap),
|
||||||
|
pool_type => random
|
||||||
|
},
|
||||||
|
#{}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, already_created} ->
|
{ok, already_created} ->
|
||||||
{ok, State};
|
{ok, State};
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
@ -154,13 +189,20 @@ update(Config, State) ->
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(Credential, #{resource_id := ResourceId,
|
authenticate(
|
||||||
method := Method,
|
Credential,
|
||||||
request_timeout := RequestTimeout} = State) ->
|
#{
|
||||||
|
resource_id := ResourceId,
|
||||||
|
method := Method,
|
||||||
|
request_timeout := RequestTimeout
|
||||||
|
} = State
|
||||||
|
) ->
|
||||||
Request = generate_request(Credential, State),
|
Request = generate_request(Credential, State),
|
||||||
case emqx_resource:query(ResourceId, {Method, Request, RequestTimeout}) of
|
case emqx_resource:query(ResourceId, {Method, Request, RequestTimeout}) of
|
||||||
{ok, 204, _Headers} -> {ok, #{is_superuser => false}};
|
{ok, 204, _Headers} ->
|
||||||
{ok, 200, _Headers} -> {ok, #{is_superuser => false}};
|
{ok, #{is_superuser => false}};
|
||||||
|
{ok, 200, _Headers} ->
|
||||||
|
{ok, #{is_superuser => false}};
|
||||||
{ok, 200, Headers, Body} ->
|
{ok, 200, Headers, Body} ->
|
||||||
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
||||||
case safely_parse_body(ContentType, Body) of
|
case safely_parse_body(ContentType, Body) of
|
||||||
|
@ -173,24 +215,32 @@ authenticate(Credential, #{resource_id := ResourceId,
|
||||||
{ok, #{is_superuser => false}}
|
{ok, #{is_superuser => false}}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "http_server_query_failed",
|
?SLOG(error, #{
|
||||||
resource => ResourceId,
|
msg => "http_server_query_failed",
|
||||||
reason => Reason}),
|
resource => ResourceId,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
ignore;
|
ignore;
|
||||||
Other ->
|
Other ->
|
||||||
Output = may_append_body(#{resource => ResourceId}, Other),
|
Output = may_append_body(#{resource => ResourceId}, Other),
|
||||||
case erlang:element(2, Other) of
|
case erlang:element(2, Other) of
|
||||||
Code5xx when Code5xx >= 500 andalso Code5xx < 600 ->
|
Code5xx when Code5xx >= 500 andalso Code5xx < 600 ->
|
||||||
?SLOG(error, Output#{msg => "http_server_error",
|
?SLOG(error, Output#{
|
||||||
code => Code5xx}),
|
msg => "http_server_error",
|
||||||
|
code => Code5xx
|
||||||
|
}),
|
||||||
ignore;
|
ignore;
|
||||||
Code4xx when Code4xx >= 400 andalso Code4xx < 500 ->
|
Code4xx when Code4xx >= 400 andalso Code4xx < 500 ->
|
||||||
?SLOG(warning, Output#{msg => "refused_by_http_server",
|
?SLOG(warning, Output#{
|
||||||
code => Code4xx}),
|
msg => "refused_by_http_server",
|
||||||
|
code => Code4xx
|
||||||
|
}),
|
||||||
{error, not_authorized};
|
{error, not_authorized};
|
||||||
OtherCode ->
|
OtherCode ->
|
||||||
?SLOG(error, Output#{msg => "undesired_response_code",
|
?SLOG(error, Output#{
|
||||||
code => OtherCode}),
|
msg => "undesired_response_code",
|
||||||
|
code => OtherCode
|
||||||
|
}),
|
||||||
ignore
|
ignore
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
@ -207,22 +257,29 @@ parse_fullpath(RawURL) ->
|
||||||
cow_http:parse_fullpath(to_bin(RawURL)).
|
cow_http:parse_fullpath(to_bin(RawURL)).
|
||||||
|
|
||||||
default_headers() ->
|
default_headers() ->
|
||||||
maps:put(<<"content-type">>,
|
maps:put(
|
||||||
<<"application/json">>,
|
<<"content-type">>,
|
||||||
default_headers_no_content_type()).
|
<<"application/json">>,
|
||||||
|
default_headers_no_content_type()
|
||||||
|
).
|
||||||
|
|
||||||
default_headers_no_content_type() ->
|
default_headers_no_content_type() ->
|
||||||
#{ <<"accept">> => <<"application/json">>
|
#{
|
||||||
, <<"cache-control">> => <<"no-cache">>
|
<<"accept">> => <<"application/json">>,
|
||||||
, <<"connection">> => <<"keep-alive">>
|
<<"cache-control">> => <<"no-cache">>,
|
||||||
, <<"keep-alive">> => <<"timeout=30, max=1000">>
|
<<"connection">> => <<"keep-alive">>,
|
||||||
}.
|
<<"keep-alive">> => <<"timeout=30, max=1000">>
|
||||||
|
}.
|
||||||
|
|
||||||
transform_header_name(Headers) ->
|
transform_header_name(Headers) ->
|
||||||
maps:fold(fun(K0, V, Acc) ->
|
maps:fold(
|
||||||
K = list_to_binary(string:to_lower(to_list(K0))),
|
fun(K0, V, Acc) ->
|
||||||
maps:put(K, V, Acc)
|
K = list_to_binary(string:to_lower(to_list(K0))),
|
||||||
end, #{}, Headers).
|
maps:put(K, V, Acc)
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Headers
|
||||||
|
).
|
||||||
|
|
||||||
check_ssl_opts(Conf) ->
|
check_ssl_opts(Conf) ->
|
||||||
{BaseUrlWithPath, _Query} = parse_fullpath(get_conf_val("url", Conf)),
|
{BaseUrlWithPath, _Query} = parse_fullpath(get_conf_val("url", Conf)),
|
||||||
|
@ -250,11 +307,13 @@ parse_url(URL) ->
|
||||||
URIMap
|
URIMap
|
||||||
end.
|
end.
|
||||||
|
|
||||||
generate_request(Credential, #{method := Method,
|
generate_request(Credential, #{
|
||||||
path := Path,
|
method := Method,
|
||||||
base_query_template := BaseQueryTemplate,
|
path := Path,
|
||||||
headers := Headers,
|
base_query_template := BaseQueryTemplate,
|
||||||
body_template := BodyTemplate}) ->
|
headers := Headers,
|
||||||
|
body_template := BodyTemplate
|
||||||
|
}) ->
|
||||||
Body = emqx_authn_utils:render_deep(BodyTemplate, Credential),
|
Body = emqx_authn_utils:render_deep(BodyTemplate, Credential),
|
||||||
NBaseQuery = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential),
|
NBaseQuery = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential),
|
||||||
case Method of
|
case Method of
|
||||||
|
|
|
@ -22,23 +22,25 @@
|
||||||
-include_lib("jose/include/jose_jwk.hrl").
|
-include_lib("jose/include/jose_jwk.hrl").
|
||||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
|
-export([
|
||||||
|
start_link/1,
|
||||||
|
stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ start_link/1
|
-export([
|
||||||
, stop/1
|
get_jwks/1,
|
||||||
]).
|
update/2
|
||||||
|
]).
|
||||||
-export([ get_jwks/1
|
|
||||||
, update/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
-export([ init/1
|
-export([
|
||||||
, handle_call/3
|
init/1,
|
||||||
, handle_cast/2
|
handle_call/3,
|
||||||
, handle_info/2
|
handle_cast/2,
|
||||||
, terminate/2
|
handle_info/2,
|
||||||
, code_change/3
|
terminate/2,
|
||||||
]).
|
code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
|
@ -67,11 +69,9 @@ init([Opts]) ->
|
||||||
|
|
||||||
handle_call(get_cached_jwks, _From, #{jwks := Jwks} = State) ->
|
handle_call(get_cached_jwks, _From, #{jwks := Jwks} = State) ->
|
||||||
{reply, {ok, Jwks}, State};
|
{reply, {ok, Jwks}, State};
|
||||||
|
|
||||||
handle_call({update, Opts}, _From, _State) ->
|
handle_call({update, Opts}, _From, _State) ->
|
||||||
NewState = handle_options(Opts),
|
NewState = handle_options(Opts),
|
||||||
{reply, ok, refresh_jwks(NewState)};
|
{reply, ok, refresh_jwks(NewState)};
|
||||||
|
|
||||||
handle_call(_Req, _From, State) ->
|
handle_call(_Req, _From, State) ->
|
||||||
{reply, ok, State}.
|
{reply, ok, State}.
|
||||||
|
|
||||||
|
@ -80,47 +80,53 @@ handle_cast(_Msg, State) ->
|
||||||
|
|
||||||
handle_info({refresh_jwks, _TRef, refresh}, #{request_id := RequestID} = State) ->
|
handle_info({refresh_jwks, _TRef, refresh}, #{request_id := RequestID} = State) ->
|
||||||
case RequestID of
|
case RequestID of
|
||||||
undefined -> ok;
|
undefined ->
|
||||||
|
ok;
|
||||||
_ ->
|
_ ->
|
||||||
ok = httpc:cancel_request(RequestID),
|
ok = httpc:cancel_request(RequestID),
|
||||||
receive
|
receive
|
||||||
{http, _} -> ok
|
{http, _} -> ok
|
||||||
after 0 ->
|
after 0 ->
|
||||||
ok
|
ok
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
{noreply, refresh_jwks(State)};
|
{noreply, refresh_jwks(State)};
|
||||||
|
handle_info(
|
||||||
handle_info({http, {RequestID, Result}},
|
{http, {RequestID, Result}},
|
||||||
#{request_id := RequestID, endpoint := Endpoint} = State0) ->
|
#{request_id := RequestID, endpoint := Endpoint} = State0
|
||||||
|
) ->
|
||||||
?tp(debug, jwks_endpoint_response, #{request_id => RequestID}),
|
?tp(debug, jwks_endpoint_response, #{request_id => RequestID}),
|
||||||
State1 = State0#{request_id := undefined},
|
State1 = State0#{request_id := undefined},
|
||||||
NewState = case Result of
|
NewState =
|
||||||
{error, Reason} ->
|
case Result of
|
||||||
?SLOG(warning, #{msg => "failed_to_request_jwks_endpoint",
|
{error, Reason} ->
|
||||||
endpoint => Endpoint,
|
?SLOG(warning, #{
|
||||||
reason => Reason}),
|
msg => "failed_to_request_jwks_endpoint",
|
||||||
State1;
|
endpoint => Endpoint,
|
||||||
{StatusLine, Headers, Body} ->
|
reason => Reason
|
||||||
try
|
}),
|
||||||
JWKS = jose_jwk:from(emqx_json:decode(Body, [return_maps])),
|
State1;
|
||||||
{_, JWKs} = JWKS#jose_jwk.keys,
|
{StatusLine, Headers, Body} ->
|
||||||
State1#{jwks := JWKs}
|
try
|
||||||
catch _:_ ->
|
JWKS = jose_jwk:from(emqx_json:decode(Body, [return_maps])),
|
||||||
?SLOG(warning, #{msg => "invalid_jwks_returned",
|
{_, JWKs} = JWKS#jose_jwk.keys,
|
||||||
endpoint => Endpoint,
|
State1#{jwks := JWKs}
|
||||||
status => StatusLine,
|
catch
|
||||||
headers => Headers,
|
_:_ ->
|
||||||
body => Body}),
|
?SLOG(warning, #{
|
||||||
State1
|
msg => "invalid_jwks_returned",
|
||||||
end
|
endpoint => Endpoint,
|
||||||
end,
|
status => StatusLine,
|
||||||
|
headers => Headers,
|
||||||
|
body => Body
|
||||||
|
}),
|
||||||
|
State1
|
||||||
|
end
|
||||||
|
end,
|
||||||
{noreply, NewState};
|
{noreply, NewState};
|
||||||
|
|
||||||
handle_info({http, {_, _}}, State) ->
|
handle_info({http, {_, _}}, State) ->
|
||||||
%% ignore
|
%% ignore
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_info(_Info, State) ->
|
handle_info(_Info, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
@ -135,32 +141,50 @@ code_change(_OldVsn, State, _Extra) ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
handle_options(#{endpoint := Endpoint,
|
handle_options(#{
|
||||||
refresh_interval := RefreshInterval0,
|
endpoint := Endpoint,
|
||||||
ssl_opts := SSLOpts}) ->
|
refresh_interval := RefreshInterval0,
|
||||||
#{endpoint => Endpoint,
|
ssl_opts := SSLOpts
|
||||||
refresh_interval => limit_refresh_interval(RefreshInterval0),
|
}) ->
|
||||||
ssl_opts => maps:to_list(SSLOpts),
|
#{
|
||||||
jwks => [],
|
endpoint => Endpoint,
|
||||||
request_id => undefined}.
|
refresh_interval => limit_refresh_interval(RefreshInterval0),
|
||||||
|
ssl_opts => maps:to_list(SSLOpts),
|
||||||
|
jwks => [],
|
||||||
|
request_id => undefined
|
||||||
|
}.
|
||||||
|
|
||||||
refresh_jwks(#{endpoint := Endpoint,
|
refresh_jwks(
|
||||||
ssl_opts := SSLOpts} = State) ->
|
#{
|
||||||
HTTPOpts = [ {timeout, 5000}
|
endpoint := Endpoint,
|
||||||
, {connect_timeout, 5000}
|
ssl_opts := SSLOpts
|
||||||
, {ssl, SSLOpts}
|
} = State
|
||||||
],
|
) ->
|
||||||
NState = case httpc:request(get, {Endpoint, [{"Accept", "application/json"}]}, HTTPOpts,
|
HTTPOpts = [
|
||||||
[{body_format, binary}, {sync, false}, {receiver, self()}]) of
|
{timeout, 5000},
|
||||||
{error, Reason} ->
|
{connect_timeout, 5000},
|
||||||
?tp(warning, jwks_endpoint_request_fail, #{endpoint => Endpoint,
|
{ssl, SSLOpts}
|
||||||
http_opts => HTTPOpts,
|
],
|
||||||
reason => Reason}),
|
NState =
|
||||||
State;
|
case
|
||||||
{ok, RequestID} ->
|
httpc:request(
|
||||||
?tp(debug, jwks_endpoint_request_ok, #{request_id => RequestID}),
|
get,
|
||||||
State#{request_id := RequestID}
|
{Endpoint, [{"Accept", "application/json"}]},
|
||||||
end,
|
HTTPOpts,
|
||||||
|
[{body_format, binary}, {sync, false}, {receiver, self()}]
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{error, Reason} ->
|
||||||
|
?tp(warning, jwks_endpoint_request_fail, #{
|
||||||
|
endpoint => Endpoint,
|
||||||
|
http_opts => HTTPOpts,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
State;
|
||||||
|
{ok, RequestID} ->
|
||||||
|
?tp(debug, jwks_endpoint_request_ok, #{request_id => RequestID}),
|
||||||
|
State#{request_id := RequestID}
|
||||||
|
end,
|
||||||
ensure_expiry_timer(NState).
|
ensure_expiry_timer(NState).
|
||||||
|
|
||||||
ensure_expiry_timer(State = #{refresh_interval := Interval}) ->
|
ensure_expiry_timer(State = #{refresh_interval := Interval}) ->
|
||||||
|
|
|
@ -23,17 +23,19 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
-behaviour(emqx_authentication).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ refs/0
|
-export([
|
||||||
, create/2
|
refs/0,
|
||||||
, update/2
|
create/2,
|
||||||
, authenticate/2
|
update/2,
|
||||||
, destroy/1
|
authenticate/2,
|
||||||
]).
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Hocon Schema
|
%% Hocon Schema
|
||||||
|
@ -42,49 +44,56 @@
|
||||||
namespace() -> "authn-jwt".
|
namespace() -> "authn-jwt".
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[ {?CONF_NS,
|
[
|
||||||
hoconsc:mk(hoconsc:union(refs()),
|
{?CONF_NS,
|
||||||
#{})}
|
hoconsc:mk(
|
||||||
|
hoconsc:union(refs()),
|
||||||
|
#{}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
fields('hmac-based') ->
|
fields('hmac-based') ->
|
||||||
[ {use_jwks, {enum, [false]}}
|
[
|
||||||
, {algorithm, {enum, ['hmac-based']}}
|
{use_jwks, {enum, [false]}},
|
||||||
, {secret, fun secret/1}
|
{algorithm, {enum, ['hmac-based']}},
|
||||||
, {secret_base64_encoded, fun secret_base64_encoded/1}
|
{secret, fun secret/1},
|
||||||
|
{secret_base64_encoded, fun secret_base64_encoded/1}
|
||||||
] ++ common_fields();
|
] ++ common_fields();
|
||||||
|
|
||||||
fields('public-key') ->
|
fields('public-key') ->
|
||||||
[ {use_jwks, {enum, [false]}}
|
[
|
||||||
, {algorithm, {enum, ['public-key']}}
|
{use_jwks, {enum, [false]}},
|
||||||
, {certificate, fun certificate/1}
|
{algorithm, {enum, ['public-key']}},
|
||||||
|
{certificate, fun certificate/1}
|
||||||
] ++ common_fields();
|
] ++ common_fields();
|
||||||
|
|
||||||
fields('jwks') ->
|
fields('jwks') ->
|
||||||
[ {use_jwks, {enum, [true]}}
|
[
|
||||||
, {endpoint, fun endpoint/1}
|
{use_jwks, {enum, [true]}},
|
||||||
, {refresh_interval, fun refresh_interval/1}
|
{endpoint, fun endpoint/1},
|
||||||
, {ssl, #{type => hoconsc:union([ hoconsc:ref(?MODULE, ssl_enable)
|
{refresh_interval, fun refresh_interval/1},
|
||||||
, hoconsc:ref(?MODULE, ssl_disable)
|
{ssl, #{
|
||||||
]),
|
type => hoconsc:union([
|
||||||
default => #{<<"enable">> => false}}}
|
hoconsc:ref(?MODULE, ssl_enable),
|
||||||
|
hoconsc:ref(?MODULE, ssl_disable)
|
||||||
|
]),
|
||||||
|
default => #{<<"enable">> => false}
|
||||||
|
}}
|
||||||
] ++ common_fields();
|
] ++ common_fields();
|
||||||
|
|
||||||
fields(ssl_enable) ->
|
fields(ssl_enable) ->
|
||||||
[ {enable, #{type => true}}
|
[
|
||||||
, {cacertfile, fun cacertfile/1}
|
{enable, #{type => true}},
|
||||||
, {certfile, fun certfile/1}
|
{cacertfile, fun cacertfile/1},
|
||||||
, {keyfile, fun keyfile/1}
|
{certfile, fun certfile/1},
|
||||||
, {verify, fun verify/1}
|
{keyfile, fun keyfile/1},
|
||||||
, {server_name_indication, fun server_name_indication/1}
|
{verify, fun verify/1},
|
||||||
|
{server_name_indication, fun server_name_indication/1}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(ssl_disable) ->
|
fields(ssl_disable) ->
|
||||||
[ {enable, #{type => false}} ].
|
[{enable, #{type => false}}].
|
||||||
|
|
||||||
common_fields() ->
|
common_fields() ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('jwt')}
|
[
|
||||||
, {verify_claims, fun verify_claims/1}
|
{mechanism, emqx_authn_schema:mechanism('jwt')},
|
||||||
|
{verify_claims, fun verify_claims/1}
|
||||||
] ++ emqx_authn_schema:common_fields().
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
secret(type) -> binary();
|
secret(type) -> binary();
|
||||||
|
@ -121,24 +130,29 @@ verify(_) -> undefined.
|
||||||
server_name_indication(type) -> string();
|
server_name_indication(type) -> string();
|
||||||
server_name_indication(_) -> undefined.
|
server_name_indication(_) -> undefined.
|
||||||
|
|
||||||
verify_claims(type) -> list();
|
verify_claims(type) ->
|
||||||
verify_claims(default) -> #{};
|
list();
|
||||||
verify_claims(validator) -> [fun do_check_verify_claims/1];
|
verify_claims(default) ->
|
||||||
|
#{};
|
||||||
|
verify_claims(validator) ->
|
||||||
|
[fun do_check_verify_claims/1];
|
||||||
verify_claims(converter) ->
|
verify_claims(converter) ->
|
||||||
fun(VerifyClaims) ->
|
fun(VerifyClaims) ->
|
||||||
[{to_binary(K), V} || {K, V} <- maps:to_list(VerifyClaims)]
|
[{to_binary(K), V} || {K, V} <- maps:to_list(VerifyClaims)]
|
||||||
end;
|
end;
|
||||||
verify_claims(_) -> undefined.
|
verify_claims(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
[ hoconsc:ref(?MODULE, 'hmac-based')
|
[
|
||||||
, hoconsc:ref(?MODULE, 'public-key')
|
hoconsc:ref(?MODULE, 'hmac-based'),
|
||||||
, hoconsc:ref(?MODULE, 'jwks')
|
hoconsc:ref(?MODULE, 'public-key'),
|
||||||
].
|
hoconsc:ref(?MODULE, 'jwks')
|
||||||
|
].
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
@ -146,18 +160,22 @@ create(_AuthenticatorID, Config) ->
|
||||||
create(#{verify_claims := VerifyClaims} = Config) ->
|
create(#{verify_claims := VerifyClaims} = Config) ->
|
||||||
create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
|
create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
|
||||||
|
|
||||||
update(#{use_jwks := false} = Config,
|
update(
|
||||||
#{jwk := Connector})
|
#{use_jwks := false} = Config,
|
||||||
when is_pid(Connector) ->
|
#{jwk := Connector}
|
||||||
|
) when
|
||||||
|
is_pid(Connector)
|
||||||
|
->
|
||||||
_ = emqx_authn_jwks_connector:stop(Connector),
|
_ = emqx_authn_jwks_connector:stop(Connector),
|
||||||
create(Config);
|
create(Config);
|
||||||
|
|
||||||
update(#{use_jwks := false} = Config, _State) ->
|
update(#{use_jwks := false} = Config, _State) ->
|
||||||
create(Config);
|
create(Config);
|
||||||
|
update(
|
||||||
update(#{use_jwks := true} = Config,
|
#{use_jwks := true} = Config,
|
||||||
#{jwk := Connector} = State)
|
#{jwk := Connector} = State
|
||||||
when is_pid(Connector) ->
|
) when
|
||||||
|
is_pid(Connector)
|
||||||
|
->
|
||||||
ok = emqx_authn_jwks_connector:update(Connector, connector_opts(Config)),
|
ok = emqx_authn_jwks_connector:update(Connector, connector_opts(Config)),
|
||||||
case maps:get(verify_cliams, Config, undefined) of
|
case maps:get(verify_cliams, Config, undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
|
@ -165,21 +183,23 @@ update(#{use_jwks := true} = Config,
|
||||||
VerifyClaims ->
|
VerifyClaims ->
|
||||||
{ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}}
|
{ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
update(#{use_jwks := true} = Config, _State) ->
|
update(#{use_jwks := true} = Config, _State) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(Credential = #{password := JWT}, #{jwk := JWK,
|
authenticate(Credential = #{password := JWT}, #{
|
||||||
verify_claims := VerifyClaims0}) ->
|
jwk := JWK,
|
||||||
JWKs = case erlang:is_pid(JWK) of
|
verify_claims := VerifyClaims0
|
||||||
false ->
|
}) ->
|
||||||
[JWK];
|
JWKs =
|
||||||
true ->
|
case erlang:is_pid(JWK) of
|
||||||
{ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK),
|
false ->
|
||||||
JWKs0
|
[JWK];
|
||||||
end,
|
true ->
|
||||||
|
{ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK),
|
||||||
|
JWKs0
|
||||||
|
end,
|
||||||
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
||||||
case verify(JWT, JWKs, VerifyClaims) of
|
case verify(JWT, JWKs, VerifyClaims) of
|
||||||
{ok, Extra} -> {ok, Extra};
|
{ok, Extra} -> {ok, Extra};
|
||||||
|
@ -197,41 +217,54 @@ destroy(_) ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
create2(#{use_jwks := false,
|
create2(#{
|
||||||
algorithm := 'hmac-based',
|
use_jwks := false,
|
||||||
secret := Secret0,
|
algorithm := 'hmac-based',
|
||||||
secret_base64_encoded := Base64Encoded,
|
secret := Secret0,
|
||||||
verify_claims := VerifyClaims}) ->
|
secret_base64_encoded := Base64Encoded,
|
||||||
|
verify_claims := VerifyClaims
|
||||||
|
}) ->
|
||||||
case may_decode_secret(Base64Encoded, Secret0) of
|
case may_decode_secret(Base64Encoded, Secret0) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
Secret ->
|
Secret ->
|
||||||
JWK = jose_jwk:from_oct(Secret),
|
JWK = jose_jwk:from_oct(Secret),
|
||||||
{ok, #{jwk => JWK,
|
{ok, #{
|
||||||
verify_claims => VerifyClaims}}
|
jwk => JWK,
|
||||||
|
verify_claims => VerifyClaims
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
|
create2(#{
|
||||||
create2(#{use_jwks := false,
|
use_jwks := false,
|
||||||
algorithm := 'public-key',
|
algorithm := 'public-key',
|
||||||
certificate := Certificate,
|
certificate := Certificate,
|
||||||
verify_claims := VerifyClaims}) ->
|
verify_claims := VerifyClaims
|
||||||
|
}) ->
|
||||||
JWK = create_jwk_from_pem_or_file(Certificate),
|
JWK = create_jwk_from_pem_or_file(Certificate),
|
||||||
{ok, #{jwk => JWK,
|
{ok, #{
|
||||||
verify_claims => VerifyClaims}};
|
jwk => JWK,
|
||||||
|
verify_claims => VerifyClaims
|
||||||
create2(#{use_jwks := true,
|
}};
|
||||||
verify_claims := VerifyClaims} = Config) ->
|
create2(
|
||||||
|
#{
|
||||||
|
use_jwks := true,
|
||||||
|
verify_claims := VerifyClaims
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
case emqx_authn_jwks_connector:start_link(connector_opts(Config)) of
|
case emqx_authn_jwks_connector:start_link(connector_opts(Config)) of
|
||||||
{ok, Connector} ->
|
{ok, Connector} ->
|
||||||
{ok, #{jwk => Connector,
|
{ok, #{
|
||||||
verify_claims => VerifyClaims}};
|
jwk => Connector,
|
||||||
|
verify_claims => VerifyClaims
|
||||||
|
}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
create_jwk_from_pem_or_file(CertfileOrFilePath)
|
create_jwk_from_pem_or_file(CertfileOrFilePath) when
|
||||||
when is_binary(CertfileOrFilePath);
|
is_binary(CertfileOrFilePath);
|
||||||
is_list(CertfileOrFilePath) ->
|
is_list(CertfileOrFilePath)
|
||||||
|
->
|
||||||
case filelib:is_file(CertfileOrFilePath) of
|
case filelib:is_file(CertfileOrFilePath) of
|
||||||
true ->
|
true ->
|
||||||
jose_jwk:from_pem_file(CertfileOrFilePath);
|
jose_jwk:from_pem_file(CertfileOrFilePath);
|
||||||
|
@ -240,18 +273,20 @@ create_jwk_from_pem_or_file(CertfileOrFilePath)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
connector_opts(#{ssl := #{enable := Enable} = SSL} = Config) ->
|
connector_opts(#{ssl := #{enable := Enable} = SSL} = Config) ->
|
||||||
SSLOpts = case Enable of
|
SSLOpts =
|
||||||
true -> maps:without([enable], SSL);
|
case Enable of
|
||||||
false -> #{}
|
true -> maps:without([enable], SSL);
|
||||||
end,
|
false -> #{}
|
||||||
|
end,
|
||||||
Config#{ssl_opts => SSLOpts}.
|
Config#{ssl_opts => SSLOpts}.
|
||||||
|
|
||||||
|
may_decode_secret(false, Secret) ->
|
||||||
may_decode_secret(false, Secret) -> Secret;
|
Secret;
|
||||||
may_decode_secret(true, Secret) ->
|
may_decode_secret(true, Secret) ->
|
||||||
try base64:decode(Secret)
|
try
|
||||||
|
base64:decode(Secret)
|
||||||
catch
|
catch
|
||||||
error : _ ->
|
error:_ ->
|
||||||
{error, {invalid_parameter, secret}}
|
{error, {invalid_parameter, secret}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -288,15 +323,18 @@ verify(JWS, [JWK | More], VerifyClaims) ->
|
||||||
|
|
||||||
verify_claims(Claims, VerifyClaims0) ->
|
verify_claims(Claims, VerifyClaims0) ->
|
||||||
Now = os:system_time(seconds),
|
Now = os:system_time(seconds),
|
||||||
VerifyClaims = [{<<"exp">>, fun(ExpireTime) ->
|
VerifyClaims =
|
||||||
Now < ExpireTime
|
[
|
||||||
end},
|
{<<"exp">>, fun(ExpireTime) ->
|
||||||
{<<"iat">>, fun(IssueAt) ->
|
Now < ExpireTime
|
||||||
IssueAt =< Now
|
end},
|
||||||
end},
|
{<<"iat">>, fun(IssueAt) ->
|
||||||
{<<"nbf">>, fun(NotBefore) ->
|
IssueAt =< Now
|
||||||
NotBefore =< Now
|
end},
|
||||||
end}] ++ VerifyClaims0,
|
{<<"nbf">>, fun(NotBefore) ->
|
||||||
|
NotBefore =< Now
|
||||||
|
end}
|
||||||
|
] ++ VerifyClaims0,
|
||||||
do_verify_claims(Claims, VerifyClaims).
|
do_verify_claims(Claims, VerifyClaims).
|
||||||
|
|
||||||
do_verify_claims(_Claims, []) ->
|
do_verify_claims(_Claims, []) ->
|
||||||
|
@ -327,8 +365,8 @@ do_check_verify_claims([]) ->
|
||||||
true;
|
true;
|
||||||
do_check_verify_claims([{Name, Expected} | More]) ->
|
do_check_verify_claims([{Name, Expected} | More]) ->
|
||||||
check_claim_name(Name) andalso
|
check_claim_name(Name) andalso
|
||||||
check_claim_expected(Expected) andalso
|
check_claim_expected(Expected) andalso
|
||||||
do_check_verify_claims(More).
|
do_check_verify_claims(More).
|
||||||
|
|
||||||
check_claim_name(exp) ->
|
check_claim_name(exp) ->
|
||||||
false;
|
false;
|
||||||
|
|
|
@ -23,40 +23,45 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
-behaviour(emqx_authentication).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ refs/0
|
-export([
|
||||||
, create/2
|
refs/0,
|
||||||
, update/2
|
create/2,
|
||||||
, authenticate/2
|
update/2,
|
||||||
, destroy/1
|
authenticate/2,
|
||||||
]).
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ import_users/2
|
-export([
|
||||||
, add_user/2
|
import_users/2,
|
||||||
, delete_user/2
|
add_user/2,
|
||||||
, update_user/3
|
delete_user/2,
|
||||||
, lookup_user/2
|
update_user/3,
|
||||||
, list_users/2
|
lookup_user/2,
|
||||||
]).
|
list_users/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ query/4
|
-export([
|
||||||
, format_user_info/1
|
query/4,
|
||||||
, group_match_spec/1]).
|
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().
|
||||||
-type user_id() :: binary().
|
-type user_id() :: binary().
|
||||||
|
|
||||||
-record(user_info,
|
-record(user_info, {
|
||||||
{ user_id :: {user_group(), user_id()}
|
user_id :: {user_group(), user_id()},
|
||||||
, password_hash :: binary()
|
password_hash :: binary(),
|
||||||
, salt :: binary()
|
salt :: binary(),
|
||||||
, is_superuser :: boolean()
|
is_superuser :: boolean()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-reflect_type([user_id_type/0]).
|
-reflect_type([user_id_type/0]).
|
||||||
|
|
||||||
|
@ -65,9 +70,11 @@
|
||||||
-boot_mnesia({mnesia, [boot]}).
|
-boot_mnesia({mnesia, [boot]}).
|
||||||
|
|
||||||
-define(TAB, ?MODULE).
|
-define(TAB, ?MODULE).
|
||||||
-define(AUTHN_QSCHEMA, [ {<<"like_username">>, binary}
|
-define(AUTHN_QSCHEMA, [
|
||||||
, {<<"like_clientid">>, binary}
|
{<<"like_username">>, binary},
|
||||||
, {<<"user_group">>, binary}]).
|
{<<"like_clientid">>, binary},
|
||||||
|
{<<"user_group">>, binary}
|
||||||
|
]).
|
||||||
-define(QUERY_FUN, {?MODULE, query}).
|
-define(QUERY_FUN, {?MODULE, query}).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -75,14 +82,15 @@
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
%% @doc Create or replicate tables.
|
%% @doc Create or replicate tables.
|
||||||
-spec(mnesia(boot | copy) -> ok).
|
-spec mnesia(boot | copy) -> ok.
|
||||||
mnesia(boot) ->
|
mnesia(boot) ->
|
||||||
ok = mria:create_table(?TAB, [
|
ok = mria:create_table(?TAB, [
|
||||||
{rlog_shard, ?AUTH_SHARD},
|
{rlog_shard, ?AUTH_SHARD},
|
||||||
{storage, disc_copies},
|
{storage, disc_copies},
|
||||||
{record_name, user_info},
|
{record_name, user_info},
|
||||||
{attributes, record_info(fields, user_info)},
|
{attributes, record_info(fields, user_info)},
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]).
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Hocon Schema
|
%% Hocon Schema
|
||||||
|
@ -93,10 +101,11 @@ namespace() -> "authn-builtin_db".
|
||||||
roots() -> [?CONF_NS].
|
roots() -> [?CONF_NS].
|
||||||
|
|
||||||
fields(?CONF_NS) ->
|
fields(?CONF_NS) ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
[
|
||||||
, {backend, emqx_authn_schema:backend('built_in_database')}
|
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||||
, {user_id_type, fun user_id_type/1}
|
{backend, emqx_authn_schema:backend('built_in_database')},
|
||||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_rw/1}
|
{user_id_type, fun user_id_type/1},
|
||||||
|
{password_hash_algorithm, fun emqx_authn_password_hashing:type_rw/1}
|
||||||
] ++ emqx_authn_schema:common_fields().
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
user_id_type(type) -> user_id_type();
|
user_id_type(type) -> user_id_type();
|
||||||
|
@ -108,15 +117,21 @@ user_id_type(_) -> undefined.
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
[hoconsc:ref(?MODULE, ?CONF_NS)].
|
[hoconsc:ref(?MODULE, ?CONF_NS)].
|
||||||
|
|
||||||
create(AuthenticatorID,
|
create(
|
||||||
#{user_id_type := Type,
|
AuthenticatorID,
|
||||||
password_hash_algorithm := Algorithm}) ->
|
#{
|
||||||
|
user_id_type := Type,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
}
|
||||||
|
) ->
|
||||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
State = #{user_group => AuthenticatorID,
|
State = #{
|
||||||
user_id_type => Type,
|
user_group => AuthenticatorID,
|
||||||
password_hash_algorithm => Algorithm},
|
user_id_type => Type,
|
||||||
|
password_hash_algorithm => Algorithm
|
||||||
|
},
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
|
|
||||||
update(Config, #{user_group := ID}) ->
|
update(Config, #{user_group := ID}) ->
|
||||||
|
@ -124,17 +139,24 @@ update(Config, #{user_group := ID}) ->
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(
|
||||||
#{user_group := UserGroup,
|
#{password := Password} = Credential,
|
||||||
user_id_type := Type,
|
#{
|
||||||
password_hash_algorithm := Algorithm}) ->
|
user_group := UserGroup,
|
||||||
|
user_id_type := Type,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
}
|
||||||
|
) ->
|
||||||
UserID = get_user_identity(Credential, Type),
|
UserID = get_user_identity(Credential, Type),
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
[] ->
|
[] ->
|
||||||
ignore;
|
ignore;
|
||||||
[#user_info{password_hash = PasswordHash, salt = Salt, is_superuser = IsSuperuser}] ->
|
[#user_info{password_hash = PasswordHash, salt = Salt, is_superuser = IsSuperuser}] ->
|
||||||
case emqx_authn_password_hashing:check_password(
|
case
|
||||||
Algorithm, Salt, PasswordHash, Password) of
|
emqx_authn_password_hashing:check_password(
|
||||||
|
Algorithm, Salt, PasswordHash, Password
|
||||||
|
)
|
||||||
|
of
|
||||||
true -> {ok, #{is_superuser => IsSuperuser}};
|
true -> {ok, #{is_superuser => IsSuperuser}};
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
end
|
end
|
||||||
|
@ -142,14 +164,15 @@ authenticate(#{password := Password} = Credential,
|
||||||
|
|
||||||
destroy(#{user_group := UserGroup}) ->
|
destroy(#{user_group := UserGroup}) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
ok = lists:foreach(
|
ok = lists:foreach(
|
||||||
fun(User) ->
|
fun(User) ->
|
||||||
mnesia:delete_object(?TAB, User, write)
|
mnesia:delete_object(?TAB, User, write)
|
||||||
end,
|
end,
|
||||||
mnesia:select(?TAB, group_match_spec(UserGroup), write))
|
mnesia:select(?TAB, group_match_spec(UserGroup), write)
|
||||||
end).
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
import_users(Filename0, State) ->
|
import_users(Filename0, State) ->
|
||||||
Filename = to_binary(Filename0),
|
Filename = to_binary(Filename0),
|
||||||
|
@ -164,10 +187,16 @@ import_users(Filename0, State) ->
|
||||||
{error, {unsupported_file_format, Extension}}
|
{error, {unsupported_file_format, Extension}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
add_user(#{user_id := UserID,
|
add_user(
|
||||||
password := Password} = UserInfo,
|
#{
|
||||||
#{user_group := UserGroup,
|
user_id := UserID,
|
||||||
password_hash_algorithm := Algorithm}) ->
|
password := Password
|
||||||
|
} = UserInfo,
|
||||||
|
#{
|
||||||
|
user_group := UserGroup,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
}
|
||||||
|
) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
|
@ -179,7 +208,8 @@ add_user(#{user_id := UserID,
|
||||||
[_] ->
|
[_] ->
|
||||||
{error, already_exist}
|
{error, already_exist}
|
||||||
end
|
end
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
delete_user(UserID, #{user_group := UserGroup}) ->
|
delete_user(UserID, #{user_group := UserGroup}) ->
|
||||||
trans(
|
trans(
|
||||||
|
@ -190,31 +220,44 @@ delete_user(UserID, #{user_group := UserGroup}) ->
|
||||||
[_] ->
|
[_] ->
|
||||||
mnesia:delete(?TAB, {UserGroup, UserID}, write)
|
mnesia:delete(?TAB, {UserGroup, UserID}, write)
|
||||||
end
|
end
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
update_user(UserID, UserInfo,
|
update_user(
|
||||||
#{user_group := UserGroup,
|
UserID,
|
||||||
password_hash_algorithm := Algorithm}) ->
|
UserInfo,
|
||||||
|
#{
|
||||||
|
user_group := UserGroup,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
}
|
||||||
|
) ->
|
||||||
trans(
|
trans(
|
||||||
fun() ->
|
fun() ->
|
||||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||||
[] ->
|
[] ->
|
||||||
{error, not_found};
|
{error, not_found};
|
||||||
[#user_info{ password_hash = PasswordHash
|
[
|
||||||
, salt = Salt
|
#user_info{
|
||||||
, is_superuser = IsSuperuser}] ->
|
password_hash = PasswordHash,
|
||||||
|
salt = Salt,
|
||||||
|
is_superuser = IsSuperuser
|
||||||
|
}
|
||||||
|
] ->
|
||||||
NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser),
|
NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser),
|
||||||
{NPasswordHash, NSalt} = case UserInfo of
|
{NPasswordHash, NSalt} =
|
||||||
#{password := Password} ->
|
case UserInfo of
|
||||||
emqx_authn_password_hashing:hash(
|
#{password := Password} ->
|
||||||
Algorithm, Password);
|
emqx_authn_password_hashing:hash(
|
||||||
#{} ->
|
Algorithm, Password
|
||||||
{PasswordHash, Salt}
|
);
|
||||||
end,
|
#{} ->
|
||||||
|
{PasswordHash, Salt}
|
||||||
|
end,
|
||||||
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
|
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
|
||||||
{ok, #{user_id => UserID, is_superuser => NSuperuser}}
|
{ok, #{user_id => UserID, is_superuser => NSuperuser}}
|
||||||
end
|
end
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
lookup_user(UserID, #{user_group := UserGroup}) ->
|
lookup_user(UserID, #{user_group := UserGroup}) ->
|
||||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||||
|
@ -233,14 +276,23 @@ list_users(QueryString, #{user_group := UserGroup}) ->
|
||||||
|
|
||||||
query(Tab, {QString, []}, Continuation, Limit) ->
|
query(Tab, {QString, []}, Continuation, Limit) ->
|
||||||
Ms = ms_from_qstring(QString),
|
Ms = ms_from_qstring(QString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_user_info/1);
|
Tab,
|
||||||
|
Ms,
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_user_info/1
|
||||||
|
);
|
||||||
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
Ms = ms_from_qstring(QString),
|
Ms = ms_from_qstring(QString),
|
||||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_user_info/1).
|
Tab,
|
||||||
|
{Ms, FuzzyFilterFun},
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_user_info/1
|
||||||
|
).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Match funcs
|
%% Match funcs
|
||||||
|
@ -248,17 +300,23 @@ query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
%% Fuzzy username funcs
|
%% Fuzzy username funcs
|
||||||
fuzzy_filter_fun(Fuzzy) ->
|
fuzzy_filter_fun(Fuzzy) ->
|
||||||
fun(MsRaws) when is_list(MsRaws) ->
|
fun(MsRaws) when is_list(MsRaws) ->
|
||||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
lists:filter(
|
||||||
, MsRaws)
|
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||||
|
MsRaws
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run_fuzzy_filter(_, []) ->
|
run_fuzzy_filter(_, []) ->
|
||||||
true;
|
true;
|
||||||
run_fuzzy_filter( E = #user_info{user_id = {_, UserID}}
|
run_fuzzy_filter(
|
||||||
, [{username, like, UsernameSubStr} | Fuzzy]) ->
|
E = #user_info{user_id = {_, UserID}},
|
||||||
|
[{username, like, UsernameSubStr} | Fuzzy]
|
||||||
|
) ->
|
||||||
binary:match(UserID, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy);
|
binary:match(UserID, UsernameSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy);
|
||||||
run_fuzzy_filter( E = #user_info{user_id = {_, UserID}}
|
run_fuzzy_filter(
|
||||||
, [{clientid, like, ClientIDSubStr} | Fuzzy]) ->
|
E = #user_info{user_id = {_, UserID}},
|
||||||
|
[{clientid, like, ClientIDSubStr} | Fuzzy]
|
||||||
|
) ->
|
||||||
binary:match(UserID, ClientIDSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
binary:match(UserID, ClientIDSubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -297,9 +355,15 @@ import_users_from_csv(Filename, #{user_group := UserGroup}) ->
|
||||||
|
|
||||||
import(_UserGroup, []) ->
|
import(_UserGroup, []) ->
|
||||||
ok;
|
ok;
|
||||||
import(UserGroup, [#{<<"user_id">> := UserID,
|
import(UserGroup, [
|
||||||
<<"password_hash">> := PasswordHash} = UserInfo | More])
|
#{
|
||||||
when is_binary(UserID) andalso is_binary(PasswordHash) ->
|
<<"user_id">> := UserID,
|
||||||
|
<<"password_hash">> := PasswordHash
|
||||||
|
} = UserInfo
|
||||||
|
| More
|
||||||
|
]) when
|
||||||
|
is_binary(UserID) andalso is_binary(PasswordHash)
|
||||||
|
->
|
||||||
Salt = maps:get(<<"salt">>, UserInfo, <<>>),
|
Salt = maps:get(<<"salt">>, UserInfo, <<>>),
|
||||||
IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false),
|
IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false),
|
||||||
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
|
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
|
||||||
|
@ -313,8 +377,11 @@ import(UserGroup, File, Seq) ->
|
||||||
{ok, Line} ->
|
{ok, Line} ->
|
||||||
Fields = binary:split(Line, [<<",">>, <<" ">>, <<"\n">>], [global, trim_all]),
|
Fields = binary:split(Line, [<<",">>, <<" ">>, <<"\n">>], [global, trim_all]),
|
||||||
case get_user_info_by_seq(Fields, Seq) of
|
case get_user_info_by_seq(Fields, Seq) of
|
||||||
{ok, #{user_id := UserID,
|
{ok,
|
||||||
password_hash := PasswordHash} = UserInfo} ->
|
#{
|
||||||
|
user_id := UserID,
|
||||||
|
password_hash := PasswordHash
|
||||||
|
} = UserInfo} ->
|
||||||
Salt = maps:get(salt, UserInfo, <<>>),
|
Salt = maps:get(salt, UserInfo, <<>>),
|
||||||
IsSuperuser = maps:get(is_superuser, UserInfo, false),
|
IsSuperuser = maps:get(is_superuser, UserInfo, false),
|
||||||
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
|
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
|
||||||
|
@ -360,10 +427,12 @@ get_user_info_by_seq(_, _, _) ->
|
||||||
{error, bad_format}.
|
{error, bad_format}.
|
||||||
|
|
||||||
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) ->
|
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) ->
|
||||||
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
UserInfo = #user_info{
|
||||||
password_hash = PasswordHash,
|
user_id = {UserGroup, UserID},
|
||||||
salt = Salt,
|
password_hash = PasswordHash,
|
||||||
is_superuser = IsSuperuser},
|
salt = Salt,
|
||||||
|
is_superuser = IsSuperuser
|
||||||
|
},
|
||||||
mnesia:write(?TAB, UserInfo, write).
|
mnesia:write(?TAB, UserInfo, write).
|
||||||
|
|
||||||
%% TODO: Support other type
|
%% TODO: Support other type
|
||||||
|
@ -392,15 +461,21 @@ 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_from_qstring(QString) ->
|
||||||
[Ms] = lists:foldl(fun({user_group, '=:=', UserGroup}, AccIn) ->
|
[Ms] = lists:foldl(
|
||||||
[group_match_spec(UserGroup) | AccIn];
|
fun
|
||||||
(_, AccIn) ->
|
({user_group, '=:=', UserGroup}, AccIn) ->
|
||||||
AccIn
|
[group_match_spec(UserGroup) | AccIn];
|
||||||
end, [], QString),
|
(_, AccIn) ->
|
||||||
|
AccIn
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
QString
|
||||||
|
),
|
||||||
Ms.
|
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 ->
|
||||||
User
|
User
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
|
@ -23,18 +23,20 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
-behaviour(emqx_authentication).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
, desc/1
|
fields/1,
|
||||||
]).
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ refs/0
|
-export([
|
||||||
, create/2
|
refs/0,
|
||||||
, update/2
|
create/2,
|
||||||
, authenticate/2
|
update/2,
|
||||||
, destroy/1
|
authenticate/2,
|
||||||
]).
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Hocon Schema
|
%% Hocon Schema
|
||||||
|
@ -43,16 +45,18 @@
|
||||||
namespace() -> "authn-mongodb".
|
namespace() -> "authn-mongodb".
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[ {?CONF_NS, hoconsc:mk(hoconsc:union(refs()),
|
[
|
||||||
#{})}
|
{?CONF_NS,
|
||||||
|
hoconsc:mk(
|
||||||
|
hoconsc:union(refs()),
|
||||||
|
#{}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
fields(standalone) ->
|
fields(standalone) ->
|
||||||
common_fields() ++ emqx_connector_mongo:fields(single);
|
common_fields() ++ emqx_connector_mongo:fields(single);
|
||||||
|
|
||||||
fields('replica-set') ->
|
fields('replica-set') ->
|
||||||
common_fields() ++ emqx_connector_mongo:fields(rs);
|
common_fields() ++ emqx_connector_mongo:fields(rs);
|
||||||
|
|
||||||
fields('sharded-cluster') ->
|
fields('sharded-cluster') ->
|
||||||
common_fields() ++ emqx_connector_mongo:fields(sharded).
|
common_fields() ++ emqx_connector_mongo:fields(sharded).
|
||||||
|
|
||||||
|
@ -66,26 +70,30 @@ desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
common_fields() ->
|
common_fields() ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
[
|
||||||
, {backend, emqx_authn_schema:backend(mongodb)}
|
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||||
, {collection, fun collection/1}
|
{backend, emqx_authn_schema:backend(mongodb)},
|
||||||
, {selector, fun selector/1}
|
{collection, fun collection/1},
|
||||||
, {password_hash_field, fun password_hash_field/1}
|
{selector, fun selector/1},
|
||||||
, {salt_field, fun salt_field/1}
|
{password_hash_field, fun password_hash_field/1},
|
||||||
, {is_superuser_field, fun is_superuser_field/1}
|
{salt_field, fun salt_field/1},
|
||||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
{is_superuser_field, fun is_superuser_field/1},
|
||||||
|
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||||
] ++ emqx_authn_schema:common_fields().
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
collection(type) -> binary();
|
collection(type) -> binary();
|
||||||
collection(desc) -> "Collection used to store authentication data.";
|
collection(desc) -> "Collection used to store authentication data.";
|
||||||
collection(_) -> undefined.
|
collection(_) -> undefined.
|
||||||
|
|
||||||
selector(type) -> map();
|
selector(type) ->
|
||||||
selector(desc) -> "Statement that is executed during the authentication process. "
|
map();
|
||||||
"Commands can support following wildcards:\n"
|
selector(desc) ->
|
||||||
" - `${username}`: substituted with client's username\n"
|
"Statement that is executed during the authentication process. "
|
||||||
" - `${clientid}`: substituted with the clientid";
|
"Commands can support following wildcards:\n"
|
||||||
selector(_) -> undefined.
|
" - `${username}`: substituted with client's username\n"
|
||||||
|
" - `${clientid}`: substituted with the clientid";
|
||||||
|
selector(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
password_hash_field(type) -> binary();
|
password_hash_field(type) -> binary();
|
||||||
password_hash_field(desc) -> "Document field that contains password hash.";
|
password_hash_field(desc) -> "Document field that contains password hash.";
|
||||||
|
@ -106,9 +114,10 @@ is_superuser_field(_) -> undefined.
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
[ hoconsc:ref(?MODULE, standalone)
|
[
|
||||||
, hoconsc:ref(?MODULE, 'replica-set')
|
hoconsc:ref(?MODULE, standalone),
|
||||||
, hoconsc:ref(?MODULE, 'sharded-cluster')
|
hoconsc:ref(?MODULE, 'replica-set'),
|
||||||
|
hoconsc:ref(?MODULE, 'sharded-cluster')
|
||||||
].
|
].
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
|
@ -117,24 +126,32 @@ create(_AuthenticatorID, Config) ->
|
||||||
create(#{selector := Selector} = Config) ->
|
create(#{selector := Selector} = Config) ->
|
||||||
SelectorTemplate = emqx_authn_utils:parse_deep(Selector),
|
SelectorTemplate = emqx_authn_utils:parse_deep(Selector),
|
||||||
State = maps:with(
|
State = maps:with(
|
||||||
[collection,
|
[
|
||||||
password_hash_field,
|
collection,
|
||||||
salt_field,
|
password_hash_field,
|
||||||
is_superuser_field,
|
salt_field,
|
||||||
password_hash_algorithm,
|
is_superuser_field,
|
||||||
salt_position],
|
password_hash_algorithm,
|
||||||
Config),
|
salt_position
|
||||||
|
],
|
||||||
|
Config
|
||||||
|
),
|
||||||
#{password_hash_algorithm := Algorithm} = State,
|
#{password_hash_algorithm := Algorithm} = State,
|
||||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
NState = State#{
|
NState = State#{
|
||||||
selector_template => SelectorTemplate,
|
selector_template => SelectorTemplate,
|
||||||
resource_id => ResourceId},
|
resource_id => ResourceId
|
||||||
case emqx_resource:create_local(ResourceId,
|
},
|
||||||
?RESOURCE_GROUP,
|
case
|
||||||
emqx_connector_mongo,
|
emqx_resource:create_local(
|
||||||
Config,
|
ResourceId,
|
||||||
#{}) of
|
?RESOURCE_GROUP,
|
||||||
|
emqx_connector_mongo,
|
||||||
|
Config,
|
||||||
|
#{}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, already_created} ->
|
{ok, already_created} ->
|
||||||
{ok, NState};
|
{ok, NState};
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
@ -154,30 +171,39 @@ update(Config, State) ->
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(
|
||||||
#{collection := Collection,
|
#{password := Password} = Credential,
|
||||||
selector_template := SelectorTemplate,
|
#{
|
||||||
resource_id := ResourceId} = State) ->
|
collection := Collection,
|
||||||
|
selector_template := SelectorTemplate,
|
||||||
|
resource_id := ResourceId
|
||||||
|
} = State
|
||||||
|
) ->
|
||||||
Selector = emqx_authn_utils:render_deep(SelectorTemplate, Credential),
|
Selector = emqx_authn_utils:render_deep(SelectorTemplate, Credential),
|
||||||
case emqx_resource:query(ResourceId, {find_one, Collection, Selector, #{}}) of
|
case emqx_resource:query(ResourceId, {find_one, Collection, Selector, #{}}) of
|
||||||
undefined -> ignore;
|
undefined ->
|
||||||
|
ignore;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "mongodb_query_failed",
|
?SLOG(error, #{
|
||||||
resource => ResourceId,
|
msg => "mongodb_query_failed",
|
||||||
collection => Collection,
|
resource => ResourceId,
|
||||||
selector => Selector,
|
collection => Collection,
|
||||||
reason => Reason}),
|
selector => Selector,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
ignore;
|
ignore;
|
||||||
Doc ->
|
Doc ->
|
||||||
case check_password(Password, Doc, State) of
|
case check_password(Password, Doc, State) of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, is_superuser(Doc, State)};
|
{ok, is_superuser(Doc, State)};
|
||||||
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
||||||
?SLOG(error, #{msg => "cannot_find_password_hash_field",
|
?SLOG(error, #{
|
||||||
resource => ResourceId,
|
msg => "cannot_find_password_hash_field",
|
||||||
collection => Collection,
|
resource => ResourceId,
|
||||||
selector => Selector,
|
collection => Collection,
|
||||||
password_hash_field => PasswordHashField}),
|
selector => Selector,
|
||||||
|
password_hash_field => PasswordHashField
|
||||||
|
}),
|
||||||
ignore;
|
ignore;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
|
@ -194,18 +220,23 @@ destroy(#{resource_id := ResourceId}) ->
|
||||||
|
|
||||||
check_password(undefined, _Selected, _State) ->
|
check_password(undefined, _Selected, _State) ->
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
check_password(Password,
|
check_password(
|
||||||
Doc,
|
Password,
|
||||||
#{password_hash_algorithm := Algorithm,
|
Doc,
|
||||||
password_hash_field := PasswordHashField} = State) ->
|
#{
|
||||||
|
password_hash_algorithm := Algorithm,
|
||||||
|
password_hash_field := PasswordHashField
|
||||||
|
} = State
|
||||||
|
) ->
|
||||||
case maps:get(PasswordHashField, Doc, undefined) of
|
case maps:get(PasswordHashField, Doc, undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
||||||
Hash ->
|
Hash ->
|
||||||
Salt = case maps:get(salt_field, State, undefined) of
|
Salt =
|
||||||
undefined -> <<>>;
|
case maps:get(salt_field, State, undefined) of
|
||||||
SaltField -> maps:get(SaltField, Doc, <<>>)
|
undefined -> <<>>;
|
||||||
end,
|
SaltField -> maps:get(SaltField, Doc, <<>>)
|
||||||
|
end,
|
||||||
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
|
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
|
|
|
@ -23,17 +23,19 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
-behaviour(emqx_authentication).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ refs/0
|
-export([
|
||||||
, create/2
|
refs/0,
|
||||||
, update/2
|
create/2,
|
||||||
, authenticate/2
|
update/2,
|
||||||
, destroy/1
|
authenticate/2,
|
||||||
]).
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Hocon Schema
|
%% Hocon Schema
|
||||||
|
@ -44,13 +46,14 @@ namespace() -> "authn-mysql".
|
||||||
roots() -> [?CONF_NS].
|
roots() -> [?CONF_NS].
|
||||||
|
|
||||||
fields(?CONF_NS) ->
|
fields(?CONF_NS) ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
[
|
||||||
, {backend, emqx_authn_schema:backend(mysql)}
|
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
{backend, emqx_authn_schema:backend(mysql)},
|
||||||
, {query, fun query/1}
|
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1},
|
||||||
, {query_timeout, fun query_timeout/1}
|
{query, fun query/1},
|
||||||
] ++ emqx_authn_schema:common_fields()
|
{query_timeout, fun query_timeout/1}
|
||||||
++ emqx_connector_mysql:fields(config).
|
] ++ emqx_authn_schema:common_fields() ++
|
||||||
|
emqx_connector_mysql:fields(config).
|
||||||
|
|
||||||
query(type) -> string();
|
query(type) -> string();
|
||||||
query(_) -> undefined.
|
query(_) -> undefined.
|
||||||
|
@ -64,28 +67,37 @@ query_timeout(_) -> undefined.
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
[hoconsc:ref(?MODULE, ?CONF_NS)].
|
[hoconsc:ref(?MODULE, ?CONF_NS)].
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
create(#{password_hash_algorithm := Algorithm,
|
create(
|
||||||
query := Query0,
|
#{
|
||||||
query_timeout := QueryTimeout
|
password_hash_algorithm := Algorithm,
|
||||||
} = Config) ->
|
query := Query0,
|
||||||
|
query_timeout := QueryTimeout
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
{Query, PlaceHolders} = emqx_authn_utils:parse_sql(Query0, '?'),
|
{Query, PlaceHolders} = emqx_authn_utils:parse_sql(Query0, '?'),
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
State = #{password_hash_algorithm => Algorithm,
|
State = #{
|
||||||
query => Query,
|
password_hash_algorithm => Algorithm,
|
||||||
placeholders => PlaceHolders,
|
query => Query,
|
||||||
query_timeout => QueryTimeout,
|
placeholders => PlaceHolders,
|
||||||
resource_id => ResourceId},
|
query_timeout => QueryTimeout,
|
||||||
case emqx_resource:create_local(ResourceId,
|
resource_id => ResourceId
|
||||||
?RESOURCE_GROUP,
|
},
|
||||||
emqx_connector_mysql,
|
case
|
||||||
Config,
|
emqx_resource:create_local(
|
||||||
#{}) of
|
ResourceId,
|
||||||
|
?RESOURCE_GROUP,
|
||||||
|
emqx_connector_mysql,
|
||||||
|
Config,
|
||||||
|
#{}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, already_created} ->
|
{ok, already_created} ->
|
||||||
{ok, State};
|
{ok, State};
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
@ -105,31 +117,41 @@ update(Config, State) ->
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(
|
||||||
#{placeholders := PlaceHolders,
|
#{password := Password} = Credential,
|
||||||
query := Query,
|
#{
|
||||||
query_timeout := Timeout,
|
placeholders := PlaceHolders,
|
||||||
resource_id := ResourceId,
|
query := Query,
|
||||||
password_hash_algorithm := Algorithm}) ->
|
query_timeout := Timeout,
|
||||||
|
resource_id := ResourceId,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
}
|
||||||
|
) ->
|
||||||
Params = emqx_authn_utils:render_sql_params(PlaceHolders, Credential),
|
Params = emqx_authn_utils:render_sql_params(PlaceHolders, Credential),
|
||||||
case emqx_resource:query(ResourceId, {sql, Query, Params, Timeout}) of
|
case emqx_resource:query(ResourceId, {sql, Query, Params, Timeout}) of
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, _Columns, []} ->
|
||||||
|
ignore;
|
||||||
{ok, Columns, [Row | _]} ->
|
{ok, Columns, [Row | _]} ->
|
||||||
Selected = maps:from_list(lists:zip(Columns, Row)),
|
Selected = maps:from_list(lists:zip(Columns, Row)),
|
||||||
case emqx_authn_utils:check_password_from_selected_map(
|
case
|
||||||
Algorithm, Selected, Password) of
|
emqx_authn_utils:check_password_from_selected_map(
|
||||||
|
Algorithm, Selected, Password
|
||||||
|
)
|
||||||
|
of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "mysql_query_failed",
|
?SLOG(error, #{
|
||||||
resource => ResourceId,
|
msg => "mysql_query_failed",
|
||||||
query => Query,
|
resource => ResourceId,
|
||||||
params => Params,
|
query => Query,
|
||||||
timeout => Timeout,
|
params => Params,
|
||||||
reason => Reason}),
|
timeout => Timeout,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
@ -24,17 +24,19 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
-behaviour(emqx_authentication).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ refs/0
|
-export([
|
||||||
, create/2
|
refs/0,
|
||||||
, update/2
|
create/2,
|
||||||
, authenticate/2
|
update/2,
|
||||||
, destroy/1
|
authenticate/2,
|
||||||
]).
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
@ -50,13 +52,14 @@ namespace() -> "authn-postgresql".
|
||||||
roots() -> [?CONF_NS].
|
roots() -> [?CONF_NS].
|
||||||
|
|
||||||
fields(?CONF_NS) ->
|
fields(?CONF_NS) ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
[
|
||||||
, {backend, emqx_authn_schema:backend(postgresql)}
|
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
{backend, emqx_authn_schema:backend(postgresql)},
|
||||||
, {query, fun query/1}
|
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1},
|
||||||
|
{query, fun query/1}
|
||||||
] ++
|
] ++
|
||||||
emqx_authn_schema:common_fields() ++
|
emqx_authn_schema:common_fields() ++
|
||||||
proplists:delete(named_queries, emqx_connector_pgsql:fields(config)).
|
proplists:delete(named_queries, emqx_connector_pgsql:fields(config)).
|
||||||
|
|
||||||
query(type) -> string();
|
query(type) -> string();
|
||||||
query(_) -> undefined.
|
query(_) -> undefined.
|
||||||
|
@ -71,17 +74,29 @@ refs() ->
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
create(#{query := Query0,
|
create(
|
||||||
password_hash_algorithm := Algorithm} = Config) ->
|
#{
|
||||||
|
query := Query0,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
{Query, PlaceHolders} = emqx_authn_utils:parse_sql(Query0, '$n'),
|
{Query, PlaceHolders} = emqx_authn_utils:parse_sql(Query0, '$n'),
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
State = #{placeholders => PlaceHolders,
|
State = #{
|
||||||
password_hash_algorithm => Algorithm,
|
placeholders => PlaceHolders,
|
||||||
resource_id => ResourceId},
|
password_hash_algorithm => Algorithm,
|
||||||
case emqx_resource:create_local(ResourceId, ?RESOURCE_GROUP, emqx_connector_pgsql,
|
resource_id => ResourceId
|
||||||
Config#{named_queries => #{ResourceId => Query}},
|
},
|
||||||
#{}) of
|
case
|
||||||
|
emqx_resource:create_local(
|
||||||
|
ResourceId,
|
||||||
|
?RESOURCE_GROUP,
|
||||||
|
emqx_connector_pgsql,
|
||||||
|
Config#{named_queries => #{ResourceId => Query}},
|
||||||
|
#{}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, already_created} ->
|
{ok, already_created} ->
|
||||||
{ok, State};
|
{ok, State};
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
@ -101,28 +116,38 @@ update(Config, State) ->
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(
|
||||||
#{placeholders := PlaceHolders,
|
#{password := Password} = Credential,
|
||||||
resource_id := ResourceId,
|
#{
|
||||||
password_hash_algorithm := Algorithm}) ->
|
placeholders := PlaceHolders,
|
||||||
|
resource_id := ResourceId,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
}
|
||||||
|
) ->
|
||||||
Params = emqx_authn_utils:render_sql_params(PlaceHolders, Credential),
|
Params = emqx_authn_utils:render_sql_params(PlaceHolders, Credential),
|
||||||
case emqx_resource:query(ResourceId, {prepared_query, ResourceId, Params}) of
|
case emqx_resource:query(ResourceId, {prepared_query, ResourceId, Params}) of
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, _Columns, []} ->
|
||||||
|
ignore;
|
||||||
{ok, Columns, [Row | _]} ->
|
{ok, Columns, [Row | _]} ->
|
||||||
NColumns = [Name || #column{name = Name} <- Columns],
|
NColumns = [Name || #column{name = Name} <- Columns],
|
||||||
Selected = maps:from_list(lists:zip(NColumns, erlang:tuple_to_list(Row))),
|
Selected = maps:from_list(lists:zip(NColumns, erlang:tuple_to_list(Row))),
|
||||||
case emqx_authn_utils:check_password_from_selected_map(
|
case
|
||||||
Algorithm, Selected, Password) of
|
emqx_authn_utils:check_password_from_selected_map(
|
||||||
|
Algorithm, Selected, Password
|
||||||
|
)
|
||||||
|
of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "postgresql_query_failed",
|
?SLOG(error, #{
|
||||||
resource => ResourceId,
|
msg => "postgresql_query_failed",
|
||||||
params => Params,
|
resource => ResourceId,
|
||||||
reason => Reason}),
|
params => Params,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
@ -23,17 +23,19 @@
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
-behaviour(emqx_authentication).
|
-behaviour(emqx_authentication).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ refs/0
|
-export([
|
||||||
, create/2
|
refs/0,
|
||||||
, update/2
|
create/2,
|
||||||
, authenticate/2
|
update/2,
|
||||||
, destroy/1
|
authenticate/2,
|
||||||
]).
|
destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Hocon Schema
|
%% Hocon Schema
|
||||||
|
@ -42,24 +44,27 @@
|
||||||
namespace() -> "authn-redis".
|
namespace() -> "authn-redis".
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[ {?CONF_NS, hoconsc:mk(hoconsc:union(refs()),
|
[
|
||||||
#{})}
|
{?CONF_NS,
|
||||||
|
hoconsc:mk(
|
||||||
|
hoconsc:union(refs()),
|
||||||
|
#{}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
fields(standalone) ->
|
fields(standalone) ->
|
||||||
common_fields() ++ emqx_connector_redis:fields(single);
|
common_fields() ++ emqx_connector_redis:fields(single);
|
||||||
|
|
||||||
fields(cluster) ->
|
fields(cluster) ->
|
||||||
common_fields() ++ emqx_connector_redis:fields(cluster);
|
common_fields() ++ emqx_connector_redis:fields(cluster);
|
||||||
|
|
||||||
fields(sentinel) ->
|
fields(sentinel) ->
|
||||||
common_fields() ++ emqx_connector_redis:fields(sentinel).
|
common_fields() ++ emqx_connector_redis:fields(sentinel).
|
||||||
|
|
||||||
common_fields() ->
|
common_fields() ->
|
||||||
[ {mechanism, emqx_authn_schema:mechanism('password_based')}
|
[
|
||||||
, {backend, emqx_authn_schema:backend(redis)}
|
{mechanism, emqx_authn_schema:mechanism('password_based')},
|
||||||
, {cmd, fun cmd/1}
|
{backend, emqx_authn_schema:backend(redis)},
|
||||||
, {password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
{cmd, fun cmd/1},
|
||||||
|
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
||||||
] ++ emqx_authn_schema:common_fields().
|
] ++ emqx_authn_schema:common_fields().
|
||||||
|
|
||||||
cmd(type) -> string();
|
cmd(type) -> string();
|
||||||
|
@ -70,30 +75,43 @@ cmd(_) -> undefined.
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
refs() ->
|
refs() ->
|
||||||
[ hoconsc:ref(?MODULE, standalone)
|
[
|
||||||
, hoconsc:ref(?MODULE, cluster)
|
hoconsc:ref(?MODULE, standalone),
|
||||||
, hoconsc:ref(?MODULE, sentinel)
|
hoconsc:ref(?MODULE, cluster),
|
||||||
|
hoconsc:ref(?MODULE, sentinel)
|
||||||
].
|
].
|
||||||
|
|
||||||
create(_AuthenticatorID, Config) ->
|
create(_AuthenticatorID, Config) ->
|
||||||
create(Config).
|
create(Config).
|
||||||
|
|
||||||
create(#{cmd := Cmd,
|
create(
|
||||||
password_hash_algorithm := Algorithm} = Config) ->
|
#{
|
||||||
|
cmd := Cmd,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
ok = emqx_authn_password_hashing:init(Algorithm),
|
ok = emqx_authn_password_hashing:init(Algorithm),
|
||||||
try
|
try
|
||||||
NCmd = parse_cmd(Cmd),
|
NCmd = parse_cmd(Cmd),
|
||||||
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
|
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
|
||||||
State = maps:with(
|
State = maps:with(
|
||||||
[password_hash_algorithm, salt_position],
|
[password_hash_algorithm, salt_position],
|
||||||
Config),
|
Config
|
||||||
|
),
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
NState = State#{
|
NState = State#{
|
||||||
cmd => NCmd,
|
cmd => NCmd,
|
||||||
resource_id => ResourceId},
|
resource_id => ResourceId
|
||||||
case emqx_resource:create_local(ResourceId, ?RESOURCE_GROUP,
|
},
|
||||||
emqx_connector_redis, Config,
|
case
|
||||||
#{}) of
|
emqx_resource:create_local(
|
||||||
|
ResourceId,
|
||||||
|
?RESOURCE_GROUP,
|
||||||
|
emqx_connector_redis,
|
||||||
|
Config,
|
||||||
|
#{}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, already_created} ->
|
{ok, already_created} ->
|
||||||
{ok, NState};
|
{ok, NState};
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
@ -121,38 +139,50 @@ update(Config, State) ->
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(
|
||||||
#{cmd := {Command, KeyTemplate, Fields},
|
#{password := Password} = Credential,
|
||||||
resource_id := ResourceId,
|
#{
|
||||||
password_hash_algorithm := Algorithm}) ->
|
cmd := {Command, KeyTemplate, Fields},
|
||||||
|
resource_id := ResourceId,
|
||||||
|
password_hash_algorithm := Algorithm
|
||||||
|
}
|
||||||
|
) ->
|
||||||
NKey = emqx_authn_utils:render_str(KeyTemplate, Credential),
|
NKey = emqx_authn_utils:render_str(KeyTemplate, Credential),
|
||||||
case emqx_resource:query(ResourceId, {cmd, [Command, NKey | Fields]}) of
|
case emqx_resource:query(ResourceId, {cmd, [Command, NKey | Fields]}) of
|
||||||
{ok, []} -> ignore;
|
{ok, []} ->
|
||||||
|
ignore;
|
||||||
{ok, Values} ->
|
{ok, Values} ->
|
||||||
case merge(Fields, Values) of
|
case merge(Fields, Values) of
|
||||||
#{<<"password_hash">> := _} = Selected ->
|
#{<<"password_hash">> := _} = Selected ->
|
||||||
case emqx_authn_utils:check_password_from_selected_map(
|
case
|
||||||
Algorithm, Selected, Password) of
|
emqx_authn_utils:check_password_from_selected_map(
|
||||||
|
Algorithm, Selected, Password
|
||||||
|
)
|
||||||
|
of
|
||||||
ok ->
|
ok ->
|
||||||
{ok, emqx_authn_utils:is_superuser(Selected)};
|
{ok, emqx_authn_utils:is_superuser(Selected)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
?SLOG(error, #{msg => "cannot_find_password_hash_field",
|
?SLOG(error, #{
|
||||||
cmd => Command,
|
msg => "cannot_find_password_hash_field",
|
||||||
keys => NKey,
|
cmd => Command,
|
||||||
fields => Fields,
|
keys => NKey,
|
||||||
resource => ResourceId}),
|
fields => Fields,
|
||||||
|
resource => ResourceId
|
||||||
|
}),
|
||||||
ignore
|
ignore
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "redis_query_failed",
|
?SLOG(error, #{
|
||||||
resource => ResourceId,
|
msg => "redis_query_failed",
|
||||||
cmd => Command,
|
resource => ResourceId,
|
||||||
keys => NKey,
|
cmd => Command,
|
||||||
fields => Fields,
|
keys => NKey,
|
||||||
reason => Reason}),
|
fields => Fields,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -191,5 +221,8 @@ merge(Fields, Value) when not is_list(Value) ->
|
||||||
merge(Fields, [Value]);
|
merge(Fields, [Value]);
|
||||||
merge(Fields, Values) ->
|
merge(Fields, Values) ->
|
||||||
maps:from_list(
|
maps:from_list(
|
||||||
[{list_to_binary(K), V}
|
[
|
||||||
|| {K, V} <- lists:zip(Fields, Values), V =/= undefined]).
|
{list_to_binary(K), V}
|
||||||
|
|| {K, V} <- lists:zip(Fields, Values), V =/= undefined
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
Loading…
Reference in New Issue