style(authn): reformat authn subdir source files

This commit is contained in:
JianBo He 2022-04-01 09:55:02 +08:00
parent 8500200e81
commit 3022ee081d
10 changed files with 1076 additions and 692 deletions

View File

@ -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
).

View File

@ -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
).

View File

@ -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

View File

@ -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}) ->

View File

@ -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;

View File

@ -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
).

View File

@ -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}

View File

@ -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.

View File

@ -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.

View File

@ -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
]
).