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

View File

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

View File

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

View File

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

View File

@ -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, []) ->

View File

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

View File

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

View File

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

View File

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

View File

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