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