style: erlfmt apps/emqx_authn

This commit is contained in:
JimMoen 2022-04-01 02:09:42 +08:00
parent 9ffc58071d
commit aae2d01582
27 changed files with 3509 additions and 2597 deletions

View File

@ -1,23 +1,32 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
{deps, {deps, [
[ {emqx, {path, "../emqx"}} {emqx, {path, "../emqx"}},
, {emqx_connector, {path, "../emqx_connector"}} {emqx_connector, {path, "../emqx_connector"}}
]}. ]}.
{edoc_opts, [{preprocess, true}]}. {edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars, {erl_opts, [
warn_shadow_vars, warn_unused_vars,
warnings_as_errors, warn_shadow_vars,
warn_unused_import, warnings_as_errors,
warn_obsolete_guard, warn_unused_import,
debug_info, warn_obsolete_guard,
{parse_transform}]}. debug_info,
{parse_transform}
]}.
{xref_checks, [undefined_function_calls, undefined_functions, {xref_checks, [
locals_not_used, deprecated_function_calls, undefined_function_calls,
warnings_as_errors, deprecated_functions]}. undefined_functions,
locals_not_used,
deprecated_function_calls,
warnings_as_errors,
deprecated_functions
]}.
{cover_enabled, true}. {cover_enabled, true}.
{cover_opts, [verbose]}. {cover_opts, [verbose]}.
{cover_export_enabled, true}. {cover_export_enabled, true}.
{project_plugins, [erlfmt]}.

View File

@ -1,13 +1,13 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
{application, emqx_authn, {application, emqx_authn, [
[{description, "EMQX Authentication"}, {description, "EMQX Authentication"},
{vsn, "0.1.0"}, {vsn, "0.1.0"},
{modules, []}, {modules, []},
{registered, [emqx_authn_sup, emqx_authn_registry]}, {registered, [emqx_authn_sup, emqx_authn_registry]},
{applications, [kernel,stdlib,emqx_resource,ehttpc,epgsql,mysql,jose]}, {applications, [kernel, stdlib, emqx_resource, ehttpc, epgsql, mysql, jose]},
{mod, {emqx_authn_app,[]}}, {mod, {emqx_authn_app, []}},
{env, []}, {env, []},
{licenses, ["Apache-2.0"]}, {licenses, ["Apache-2.0"]},
{maintainers, ["EMQX Team <contact@emqx.io>"]}, {maintainers, ["EMQX Team <contact@emqx.io>"]},
{links, [{"Homepage", "https://emqx.io/"}]} {links, [{"Homepage", "https://emqx.io/"}]}
]}. ]}.

View File

@ -16,28 +16,31 @@
-module(emqx_authn). -module(emqx_authn).
-export([ providers/0 -export([
, check_config/1 providers/0,
, check_config/2 check_config/1,
, check_configs/1 check_config/2,
]). check_configs/1
]).
-include("emqx_authn.hrl"). -include("emqx_authn.hrl").
providers() -> providers() ->
[ {{'password_based', 'built_in_database'}, emqx_authn_mnesia} [
, {{'password_based', mysql}, emqx_authn_mysql} {{'password_based', 'built_in_database'}, emqx_authn_mnesia},
, {{'password_based', postgresql}, emqx_authn_pgsql} {{'password_based', mysql}, emqx_authn_mysql},
, {{'password_based', mongodb}, emqx_authn_mongodb} {{'password_based', postgresql}, emqx_authn_pgsql},
, {{'password_based', redis}, emqx_authn_redis} {{'password_based', mongodb}, emqx_authn_mongodb},
, {{'password_based', 'http'}, emqx_authn_http} {{'password_based', redis}, emqx_authn_redis},
, {jwt, emqx_authn_jwt} {{'password_based', 'http'}, emqx_authn_http},
, {{scram, 'built_in_database'}, emqx_enhanced_authn_scram_mnesia} {jwt, emqx_authn_jwt},
{{scram, 'built_in_database'}, emqx_enhanced_authn_scram_mnesia}
]. ].
check_configs(C) when is_map(C) -> check_configs(C) when is_map(C) ->
check_configs([C]); check_configs([C]);
check_configs([]) -> []; check_configs([]) ->
[];
check_configs([Config | Configs]) -> check_configs([Config | Configs]) ->
[check_config(Config) | check_configs(Configs)]. [check_config(Config) | check_configs(Configs)].
@ -51,22 +54,26 @@ check_config(Config, Opts) ->
end. end.
do_check_config(#{<<"mechanism">> := Mec} = Config, Opts) -> do_check_config(#{<<"mechanism">> := Mec} = Config, Opts) ->
Key = case maps:get(<<"backend">>, Config, false) of Key =
false -> atom(Mec); case maps:get(<<"backend">>, Config, false) of
Backend -> {atom(Mec), atom(Backend)} false -> atom(Mec);
end, Backend -> {atom(Mec), atom(Backend)}
end,
case lists:keyfind(Key, 1, providers()) of case lists:keyfind(Key, 1, providers()) of
false -> false ->
throw({unknown_handler, Key}); throw({unknown_handler, Key});
{_, ProviderModule} -> {_, ProviderModule} ->
hocon_tconf:check_plain(ProviderModule, #{?CONF_NS_BINARY => Config}, hocon_tconf:check_plain(
Opts#{atom_key => true}) ProviderModule,
#{?CONF_NS_BINARY => Config},
Opts#{atom_key => true}
)
end. end.
atom(Bin) -> atom(Bin) ->
try try
binary_to_existing_atom(Bin, utf8) binary_to_existing_atom(Bin, utf8)
catch catch
_ : _ -> _:_ ->
throw({unknown_auth_provider, Bin}) throw({unknown_auth_provider, Bin})
end. end.

File diff suppressed because it is too large Load Diff

View File

@ -21,9 +21,10 @@
-behaviour(application). -behaviour(application).
%% Application callbacks %% Application callbacks
-export([ start/2 -export([
, stop/1 start/2,
]). stop/1
]).
-include_lib("emqx/include/emqx_authentication.hrl"). -include_lib("emqx/include/emqx_authentication.hrl").
@ -51,13 +52,15 @@ initialize() ->
ok = ?AUTHN:register_providers(emqx_authn:providers()), ok = ?AUTHN:register_providers(emqx_authn:providers()),
lists:foreach( lists:foreach(
fun({ChainName, RawAuthConfigs}) -> fun({ChainName, RawAuthConfigs}) ->
AuthConfig = emqx_authn:check_configs(RawAuthConfigs), AuthConfig = emqx_authn:check_configs(RawAuthConfigs),
?AUTHN:initialize_authentication( ?AUTHN:initialize_authentication(
ChainName, ChainName,
AuthConfig) AuthConfig
end, )
chain_configs()). end,
chain_configs()
).
deinitialize() -> deinitialize() ->
ok = ?AUTHN:deregister_providers(provider_types()), ok = ?AUTHN:deregister_providers(provider_types()),
@ -71,15 +74,16 @@ global_chain_config() ->
listener_chain_configs() -> listener_chain_configs() ->
lists:map( lists:map(
fun({ListenerID, _}) -> fun({ListenerID, _}) ->
{ListenerID, emqx:get_raw_config(auth_config_path(ListenerID), [])} {ListenerID, emqx:get_raw_config(auth_config_path(ListenerID), [])}
end, end,
emqx_listeners:list()). emqx_listeners:list()
).
auth_config_path(ListenerID) -> auth_config_path(ListenerID) ->
[<<"listeners">>] [<<"listeners">>] ++
++ binary:split(atom_to_binary(ListenerID), <<":">>) binary:split(atom_to_binary(ListenerID), <<":">>) ++
++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY]. [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY].
provider_types() -> provider_types() ->
lists:map(fun({Type, _Module}) -> Type end, emqx_authn:providers()). lists:map(fun({Type, _Module}) -> Type end, emqx_authn:providers()).

View File

@ -18,21 +18,25 @@
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-type(simple_algorithm_name() :: plain | md5 | sha | sha256 | sha512). -type simple_algorithm_name() :: plain | md5 | sha | sha256 | sha512.
-type(salt_position() :: prefix | suffix). -type salt_position() :: prefix | suffix.
-type(simple_algorithm() :: #{name := simple_algorithm_name(), -type simple_algorithm() :: #{
salt_position := salt_position()}). name := simple_algorithm_name(),
salt_position := salt_position()
}.
-type(bcrypt_algorithm() :: #{name := bcrypt}). -type bcrypt_algorithm() :: #{name := bcrypt}.
-type(bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}). -type bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}.
-type(pbkdf2_algorithm() :: #{name := pbkdf2, -type pbkdf2_algorithm() :: #{
mac_fun := emqx_passwd:pbkdf2_mac_fun(), name := pbkdf2,
iterations := pos_integer()}). mac_fun := emqx_passwd:pbkdf2_mac_fun(),
iterations := pos_integer()
}.
-type(algorithm() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm()). -type algorithm() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm().
-type(algorithm_rw() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm_rw()). -type algorithm_rw() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm_rw().
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Hocon Schema %% Hocon Schema
@ -40,37 +44,44 @@
-behaviour(hocon_schema). -behaviour(hocon_schema).
-export([roots/0, -export([
fields/1, roots/0,
namespace/0]). fields/1,
namespace/0
]).
-export([type_ro/1, -export([
type_rw/1]). type_ro/1,
type_rw/1
]).
-export([init/1, -export([
gen_salt/1, init/1,
hash/2, gen_salt/1,
check_password/4]). hash/2,
check_password/4
]).
namespace() -> "authn-hash". namespace() -> "authn-hash".
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms]. roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
fields(bcrypt_rw) -> fields(bcrypt_rw) ->
fields(bcrypt) ++ fields(bcrypt) ++
[{salt_rounds, fun salt_rounds/1}]; [{salt_rounds, fun salt_rounds/1}];
fields(bcrypt) -> fields(bcrypt) ->
[{name, {enum, [bcrypt]}}]; [{name, {enum, [bcrypt]}}];
fields(pbkdf2) -> fields(pbkdf2) ->
[{name, {enum, [pbkdf2]}}, [
{mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}}, {name, {enum, [pbkdf2]}},
{iterations, integer()}, {mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}},
{dk_length, fun dk_length/1}]; {iterations, integer()},
{dk_length, fun dk_length/1}
];
fields(other_algorithms) -> fields(other_algorithms) ->
[{name, {enum, [plain, md5, sha, sha256, sha512]}}, [
{salt_position, fun salt_position/1}]. {name, {enum, [plain, md5, sha, sha256, sha512]}},
{salt_position, fun salt_position/1}
].
salt_position(type) -> {enum, [prefix, suffix]}; salt_position(type) -> {enum, [prefix, suffix]};
salt_position(desc) -> "Specifies whether the password salt is stored as a prefix or the suffix."; salt_position(desc) -> "Specifies whether the password salt is stored as a prefix or the suffix.";
@ -89,47 +100,56 @@ dk_length(_) -> undefined.
type_rw(type) -> type_rw(type) ->
hoconsc:union(rw_refs()); hoconsc:union(rw_refs());
type_rw(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix}; type_rw(default) ->
type_rw(_) -> undefined. #{<<"name">> => sha256, <<"salt_position">> => prefix};
type_rw(_) ->
undefined.
type_ro(type) -> type_ro(type) ->
hoconsc:union(ro_refs()); hoconsc:union(ro_refs());
type_ro(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix}; type_ro(default) ->
type_ro(_) -> undefined. #{<<"name">> => sha256, <<"salt_position">> => prefix};
type_ro(_) ->
undefined.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% APIs %% APIs
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
-spec(init(algorithm()) -> ok). -spec init(algorithm()) -> ok.
init(#{name := bcrypt}) -> init(#{name := bcrypt}) ->
{ok, _} = application:ensure_all_started(bcrypt), {ok, _} = application:ensure_all_started(bcrypt),
ok; ok;
init(#{name := _Other}) -> init(#{name := _Other}) ->
ok. ok.
-spec gen_salt(algorithm_rw()) -> emqx_passwd:salt().
-spec(gen_salt(algorithm_rw()) -> emqx_passwd:salt()).
gen_salt(#{name := plain}) -> gen_salt(#{name := plain}) ->
<<>>; <<>>;
gen_salt(#{name := bcrypt, gen_salt(#{
salt_rounds := Rounds}) -> name := bcrypt,
salt_rounds := Rounds
}) ->
{ok, Salt} = bcrypt:gen_salt(Rounds), {ok, Salt} = bcrypt:gen_salt(Rounds),
list_to_binary(Salt); list_to_binary(Salt);
gen_salt(#{name := Other}) when Other =/= plain, Other =/= bcrypt -> gen_salt(#{name := Other}) when Other =/= plain, Other =/= bcrypt ->
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16), <<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
iolist_to_binary(io_lib:format("~32.16.0b", [X])). iolist_to_binary(io_lib:format("~32.16.0b", [X])).
-spec hash(algorithm_rw(), emqx_passwd:password()) -> {emqx_passwd:hash(), emqx_passwd:salt()}.
-spec(hash(algorithm_rw(), emqx_passwd:password()) -> {emqx_passwd:hash(), emqx_passwd:salt()}).
hash(#{name := bcrypt, salt_rounds := _} = Algorithm, Password) -> hash(#{name := bcrypt, salt_rounds := _} = Algorithm, Password) ->
Salt0 = gen_salt(Algorithm), Salt0 = gen_salt(Algorithm),
Hash = emqx_passwd:hash({bcrypt, Salt0}, Password), Hash = emqx_passwd:hash({bcrypt, Salt0}, Password),
Salt = Hash, Salt = Hash,
{Hash, Salt}; {Hash, Salt};
hash(#{name := pbkdf2, hash(
mac_fun := MacFun, #{
iterations := Iterations} = Algorithm, Password) -> name := pbkdf2,
mac_fun := MacFun,
iterations := Iterations
} = Algorithm,
Password
) ->
Salt = gen_salt(Algorithm), Salt = gen_salt(Algorithm),
DKLength = maps:get(dk_length, Algorithm, undefined), DKLength = maps:get(dk_length, Algorithm, undefined),
Hash = emqx_passwd:hash({pbkdf2, MacFun, Salt, Iterations, DKLength}, Password), Hash = emqx_passwd:hash({pbkdf2, MacFun, Salt, Iterations, DKLength}, Password),
@ -139,18 +159,24 @@ hash(#{name := Other, salt_position := SaltPosition} = Algorithm, Password) ->
Hash = emqx_passwd:hash({Other, Salt, SaltPosition}, Password), Hash = emqx_passwd:hash({Other, Salt, SaltPosition}, Password),
{Hash, Salt}. {Hash, Salt}.
-spec check_password(
-spec(check_password( algorithm(),
algorithm(), emqx_passwd:salt(),
emqx_passwd:salt(), emqx_passwd:hash(),
emqx_passwd:hash(), emqx_passwd:password()
emqx_passwd:password()) -> boolean()). ) -> boolean().
check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) -> check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) ->
emqx_passwd:check_pass({bcrypt, PasswordHash}, PasswordHash, Password); emqx_passwd:check_pass({bcrypt, PasswordHash}, PasswordHash, Password);
check_password(#{name := pbkdf2, check_password(
mac_fun := MacFun, #{
iterations := Iterations} = Algorithm, name := pbkdf2,
Salt, PasswordHash, Password) -> mac_fun := MacFun,
iterations := Iterations
} = Algorithm,
Salt,
PasswordHash,
Password
) ->
DKLength = maps:get(dk_length, Algorithm, undefined), DKLength = maps:get(dk_length, Algorithm, undefined),
emqx_passwd:check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password); emqx_passwd:check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password);
check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHash, Password) -> check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHash, Password) ->
@ -161,11 +187,15 @@ check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHa
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
rw_refs() -> rw_refs() ->
[hoconsc:ref(?MODULE, bcrypt_rw), [
hoconsc:ref(?MODULE, pbkdf2), hoconsc:ref(?MODULE, bcrypt_rw),
hoconsc:ref(?MODULE, other_algorithms)]. hoconsc:ref(?MODULE, pbkdf2),
hoconsc:ref(?MODULE, other_algorithms)
].
ro_refs() -> ro_refs() ->
[hoconsc:ref(?MODULE, bcrypt), [
hoconsc:ref(?MODULE, pbkdf2), hoconsc:ref(?MODULE, bcrypt),
hoconsc:ref(?MODULE, other_algorithms)]. hoconsc:ref(?MODULE, pbkdf2),
hoconsc:ref(?MODULE, other_algorithms)
].

View File

@ -20,21 +20,20 @@
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-import(hoconsc, [mk/2, ref/2]). -import(hoconsc, [mk/2, ref/2]).
-export([ common_fields/0 -export([
, roots/0 common_fields/0,
, fields/1 roots/0,
, authenticator_type/0 fields/1,
, root_type/0 authenticator_type/0,
, mechanism/1 root_type/0,
, backend/1 mechanism/1,
]). backend/1
]).
roots() -> []. roots() -> [].
common_fields() -> common_fields() ->
[ {enable, fun enable/1} [{enable, fun enable/1}].
].
enable(type) -> boolean(); enable(type) -> boolean();
enable(default) -> true; enable(default) -> true;
@ -54,46 +53,60 @@ root_type() ->
hoconsc:array(authenticator_type()). hoconsc:array(authenticator_type()).
mechanism(Name) -> mechanism(Name) ->
hoconsc:mk(hoconsc:enum([Name]), hoconsc:mk(
#{ required => true hoconsc:enum([Name]),
, desc => "Authentication mechanism." #{
}). required => true,
desc => "Authentication mechanism."
}
).
backend(Name) -> backend(Name) ->
hoconsc:mk(hoconsc:enum([Name]), hoconsc:mk(
#{ required => true hoconsc:enum([Name]),
, desc => "Backend type." #{
}). required => true,
desc => "Backend type."
}
).
fields("metrics_status_fields") -> fields("metrics_status_fields") ->
[ {"metrics", mk(ref(?MODULE, "metrics"), #{desc => "The metrics of the resource"})} [
, {"node_metrics", mk(hoconsc:array(ref(?MODULE, "node_metrics")), {"metrics", mk(ref(?MODULE, "metrics"), #{desc => "The metrics of the resource"})},
#{ desc => "The metrics of the resource for each node" {"node_metrics",
})} mk(
, {"status", mk(status(), #{desc => "The status of the resource"})} hoconsc:array(ref(?MODULE, "node_metrics")),
, {"node_status", mk(hoconsc:array(ref(?MODULE, "node_status")), #{desc => "The metrics of the resource for each node"}
#{ desc => "The status of the resource for each node" )},
})} {"status", mk(status(), #{desc => "The status of the resource"})},
{"node_status",
mk(
hoconsc:array(ref(?MODULE, "node_status")),
#{desc => "The status of the resource for each node"}
)}
]; ];
fields("metrics") -> fields("metrics") ->
[ {"matched", mk(integer(), #{desc => "Count of this resource is queried"})} [
, {"success", mk(integer(), #{desc => "Count of query success"})} {"matched", mk(integer(), #{desc => "Count of this resource is queried"})},
, {"failed", mk(integer(), #{desc => "Count of query failed"})} {"success", mk(integer(), #{desc => "Count of query success"})},
, {"rate", mk(float(), #{desc => "The rate of matched, times/second"})} {"failed", mk(integer(), #{desc => "Count of query failed"})},
, {"rate_max", mk(float(), #{desc => "The max rate of matched, times/second"})} {"rate", mk(float(), #{desc => "The rate of matched, times/second"})},
, {"rate_last5m", mk(float(), {"rate_max", mk(float(), #{desc => "The max rate of matched, times/second"})},
#{desc => "The average rate of matched in the last 5 minutes, times/second"})} {"rate_last5m",
mk(
float(),
#{desc => "The average rate of matched in the last 5 minutes, times/second"}
)}
]; ];
fields("node_metrics") -> fields("node_metrics") ->
[ node_name() [
, {"metrics", mk(ref(?MODULE, "metrics"), #{})} node_name(),
{"metrics", mk(ref(?MODULE, "metrics"), #{})}
]; ];
fields("node_status") -> fields("node_status") ->
[ node_name() [
, {"status", mk(status(), #{desc => "Status of the node."})} node_name(),
{"status", mk(status(), #{desc => "Status of the node."})}
]. ].
status() -> status() ->

View File

@ -18,9 +18,10 @@
-behaviour(supervisor). -behaviour(supervisor).
-export([ start_link/0 -export([
, init/1 start_link/0,
]). init/1
]).
start_link() -> start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).

View File

@ -19,26 +19,29 @@
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("emqx_authn.hrl"). -include_lib("emqx_authn.hrl").
-export([ check_password_from_selected_map/3 -export([
, parse_deep/1 check_password_from_selected_map/3,
, parse_str/1 parse_deep/1,
, parse_sql/2 parse_str/1,
, render_deep/2 parse_sql/2,
, render_str/2 render_deep/2,
, render_sql_params/2 render_str/2,
, is_superuser/1 render_sql_params/2,
, bin/1 is_superuser/1,
, ensure_apps_started/1 bin/1,
, cleanup_resources/0 ensure_apps_started/1,
, make_resource_id/1 cleanup_resources/0,
]). make_resource_id/1
]).
-define(AUTHN_PLACEHOLDERS, [?PH_USERNAME, -define(AUTHN_PLACEHOLDERS, [
?PH_CLIENTID, ?PH_USERNAME,
?PH_PASSWORD, ?PH_CLIENTID,
?PH_PEERHOST, ?PH_PASSWORD,
?PH_CERT_SUBJECT, ?PH_PEERHOST,
?PH_CERT_CN_NAME]). ?PH_CERT_SUBJECT,
?PH_CERT_CN_NAME
]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% APIs %% APIs
@ -47,12 +50,12 @@
check_password_from_selected_map(_Algorithm, _Selected, undefined) -> check_password_from_selected_map(_Algorithm, _Selected, undefined) ->
{error, bad_username_or_password}; {error, bad_username_or_password};
check_password_from_selected_map( check_password_from_selected_map(
Algorithm, #{<<"password_hash">> := Hash} = Selected, Password) -> Algorithm, #{<<"password_hash">> := Hash} = Selected, Password
) ->
Salt = maps:get(<<"salt">>, Selected, <<>>), Salt = maps:get(<<"salt">>, Selected, <<>>),
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
true -> ok; true -> ok;
false -> false -> {error, bad_username_or_password}
{error, bad_username_or_password}
end. end.
parse_deep(Template) -> parse_deep(Template) ->
@ -63,27 +66,33 @@ parse_str(Template) ->
parse_sql(Template, ReplaceWith) -> parse_sql(Template, ReplaceWith) ->
emqx_placeholder:preproc_sql( emqx_placeholder:preproc_sql(
Template, Template,
#{replace_with => ReplaceWith, #{
placeholders => ?AUTHN_PLACEHOLDERS}). replace_with => ReplaceWith,
placeholders => ?AUTHN_PLACEHOLDERS
}
).
render_deep(Template, Credential) -> render_deep(Template, Credential) ->
emqx_placeholder:proc_tmpl_deep( emqx_placeholder:proc_tmpl_deep(
Template, Template,
Credential, Credential,
#{return => full_binary, var_trans => fun handle_var/2}). #{return => full_binary, var_trans => fun handle_var/2}
).
render_str(Template, Credential) -> render_str(Template, Credential) ->
emqx_placeholder:proc_tmpl( emqx_placeholder:proc_tmpl(
Template, Template,
Credential, Credential,
#{return => full_binary, var_trans => fun handle_var/2}). #{return => full_binary, var_trans => fun handle_var/2}
).
render_sql_params(ParamList, Credential) -> render_sql_params(ParamList, Credential) ->
emqx_placeholder:proc_tmpl( emqx_placeholder:proc_tmpl(
ParamList, ParamList,
Credential, Credential,
#{return => rawlist, var_trans => fun handle_sql_var/2}). #{return => rawlist, var_trans => fun handle_sql_var/2}
).
is_superuser(#{<<"is_superuser">> := <<"">>}) -> is_superuser(#{<<"is_superuser">> := <<"">>}) ->
#{is_superuser => false}; #{is_superuser => false};
@ -114,8 +123,9 @@ bin(X) -> X.
cleanup_resources() -> cleanup_resources() ->
lists:foreach( lists:foreach(
fun emqx_resource:remove_local/1, fun emqx_resource:remove_local/1,
emqx_resource:list_group_instances(?RESOURCE_GROUP)). emqx_resource:list_group_instances(?RESOURCE_GROUP)
).
make_resource_id(Name) -> make_resource_id(Name) ->
NameBin = bin(Name), NameBin = bin(Name),

View File

@ -26,12 +26,12 @@
-define(TCP_DEFAULT, 'tcp:default'). -define(TCP_DEFAULT, 'tcp:default').
-define( -define(assertAuthenticatorsMatch(Guard, Path),
assertAuthenticatorsMatch(Guard, Path),
(fun() -> (fun() ->
{ok, 200, Response} = request(get, uri(Path)), {ok, 200, Response} = request(get, uri(Path)),
?assertMatch(Guard, jiffy:decode(Response, [return_maps])) ?assertMatch(Guard, jiffy:decode(Response, [return_maps]))
end)()). end)()
).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -42,12 +42,14 @@ groups() ->
init_per_testcase(_, Config) -> init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[?CONF_NS_ATOM], [?CONF_NS_ATOM],
?GLOBAL), ?GLOBAL
),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[listeners, tcp, default, ?CONF_NS_ATOM], [listeners, tcp, default, ?CONF_NS_ATOM],
?TCP_DEFAULT), ?TCP_DEFAULT
),
{atomic, ok} = mria:clear_table(emqx_authn_mnesia), {atomic, ok} = mria:clear_table(emqx_authn_mnesia),
Config. Config.
@ -55,8 +57,9 @@ init_per_testcase(_, Config) ->
init_per_suite(Config) -> init_per_suite(Config) ->
_ = application:load(emqx_conf), _ = application:load(emqx_conf),
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_authn, emqx_dashboard], [emqx_authn, emqx_dashboard],
fun set_special_configs/1), fun set_special_configs/1
),
?AUTHN:delete_chain(?GLOBAL), ?AUTHN:delete_chain(?GLOBAL),
{ok, Chains} = ?AUTHN:list_chains(), {ok, Chains} = ?AUTHN:list_chains(),
@ -117,108 +120,132 @@ t_listener_authenticator_import_users(_) ->
test_authenticator_import_users(["listeners", ?TCP_DEFAULT]). test_authenticator_import_users(["listeners", ?TCP_DEFAULT]).
test_authenticators(PathPrefix) -> test_authenticators(PathPrefix) ->
ValidConfig = emqx_authn_test_lib:http_example(), ValidConfig = emqx_authn_test_lib:http_example(),
{ok, 200, _} = request( {ok, 200, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
ValidConfig), ValidConfig
),
{ok, 409, _} = request( {ok, 409, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
ValidConfig), ValidConfig
),
InvalidConfig0 = ValidConfig#{method => <<"delete">>}, InvalidConfig0 = ValidConfig#{method => <<"delete">>},
{ok, 400, _} = request( {ok, 400, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
InvalidConfig0), InvalidConfig0
),
InvalidConfig1 = ValidConfig#{method => <<"get">>, InvalidConfig1 = ValidConfig#{
headers => #{<<"content-type">> => <<"application/json">>}}, method => <<"get">>,
headers => #{<<"content-type">> => <<"application/json">>}
},
{ok, 400, _} = request( {ok, 400, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
InvalidConfig1), InvalidConfig1
),
?assertAuthenticatorsMatch( ?assertAuthenticatorsMatch(
[#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}], [#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}],
PathPrefix ++ [?CONF_NS]). PathPrefix ++ [?CONF_NS]
).
test_authenticator(PathPrefix) -> test_authenticator(PathPrefix) ->
ValidConfig0 = emqx_authn_test_lib:http_example(), ValidConfig0 = emqx_authn_test_lib:http_example(),
{ok, 200, _} = request( {ok, 200, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
ValidConfig0), ValidConfig0
),
{ok, 200, _} = request( {ok, 200, _} = request(
get, get,
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])), uri(PathPrefix ++ [?CONF_NS, "password_based:http"])
),
{ok, 200, Res} = request( {ok, 200, Res} = request(
get, get,
uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])), uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])
),
{ok, RList} = emqx_json:safe_decode(Res), {ok, RList} = emqx_json:safe_decode(Res),
Snd = fun ({_, Val}) -> Val end, Snd = fun({_, Val}) -> Val end,
LookupVal = fun LookupV(List, RestJson) -> LookupVal = fun LookupV(List, RestJson) ->
case List of case List of
[Name] -> Snd(lists:keyfind(Name, 1, RestJson)); [Name] -> Snd(lists:keyfind(Name, 1, RestJson));
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson))) [Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
end end
end, end,
LookFun = fun (List) -> LookupVal(List, RList) end, LookFun = fun(List) -> LookupVal(List, RList) end,
MetricsList = [{<<"failed">>, 0}, MetricsList = [
{<<"matched">>, 0}, {<<"failed">>, 0},
{<<"rate">>, 0.0}, {<<"matched">>, 0},
{<<"rate_last5m">>, 0.0}, {<<"rate">>, 0.0},
{<<"rate_max">>, 0.0}, {<<"rate_last5m">>, 0.0},
{<<"success">>, 0}], {<<"rate_max">>, 0.0},
EqualFun = fun ({M, V}) -> {<<"success">>, 0}
?assertEqual(V, LookFun([<<"metrics">>, ],
M] EqualFun = fun({M, V}) ->
) ?assertEqual(
) end, V,
LookFun([
<<"metrics">>,
M
])
)
end,
lists:map(EqualFun, MetricsList), lists:map(EqualFun, MetricsList),
?assertEqual(<<"connected">>, ?assertEqual(
LookFun([<<"status">> <<"connected">>,
])), LookFun([<<"status">>])
),
{ok, 404, _} = request( {ok, 404, _} = request(
get, get,
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])), uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
),
{ok, 404, _} = request( {ok, 404, _} = request(
put, put,
uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database"]), uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database"]),
emqx_authn_test_lib:built_in_database_example()), emqx_authn_test_lib:built_in_database_example()
),
InvalidConfig0 = ValidConfig0#{method => <<"delete">>}, InvalidConfig0 = ValidConfig0#{method => <<"delete">>},
{ok, 400, _} = request( {ok, 400, _} = request(
put, put,
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]), uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
InvalidConfig0), InvalidConfig0
),
InvalidConfig1 = ValidConfig0#{method => <<"get">>, InvalidConfig1 = ValidConfig0#{
headers => #{<<"content-type">> => <<"application/json">>}}, method => <<"get">>,
headers => #{<<"content-type">> => <<"application/json">>}
},
{ok, 400, _} = request( {ok, 400, _} = request(
put, put,
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]), uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
InvalidConfig1), InvalidConfig1
),
ValidConfig1 = ValidConfig0#{pool_size => 9}, ValidConfig1 = ValidConfig0#{pool_size => 9},
{ok, 200, _} = request( {ok, 200, _} = request(
put, put,
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]), uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
ValidConfig1), ValidConfig1
),
{ok, 404, _} = request( {ok, 404, _} = request(
delete, delete,
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])), uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
),
{ok, 204, _} = request( {ok, 204, _} = request(
delete, delete,
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])), uri(PathPrefix ++ [?CONF_NS, "password_based:http"])
),
?assertAuthenticatorsMatch([], PathPrefix ++ [?CONF_NS]). ?assertAuthenticatorsMatch([], PathPrefix ++ [?CONF_NS]).
@ -226,64 +253,78 @@ test_authenticator_users(PathPrefix) ->
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]), UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
{ok, 200, _} = request( {ok, 200, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
emqx_authn_test_lib:built_in_database_example()), emqx_authn_test_lib:built_in_database_example()
),
InvalidUsers = [ InvalidUsers = [
#{clientid => <<"u1">>, password => <<"p1">>}, #{clientid => <<"u1">>, password => <<"p1">>},
#{user_id => <<"u2">>}, #{user_id => <<"u2">>},
#{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}], #{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}
],
lists:foreach( lists:foreach(
fun(User) -> {ok, 400, _} = request(post, UsersUri, User) end, fun(User) -> {ok, 400, _} = request(post, UsersUri, User) end,
InvalidUsers), InvalidUsers
),
ValidUsers = [ ValidUsers = [
#{user_id => <<"u1">>, password => <<"p1">>}, #{user_id => <<"u1">>, password => <<"p1">>},
#{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true}, #{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true},
#{user_id => <<"u3">>, password => <<"p3">>}], #{user_id => <<"u3">>, password => <<"p3">>}
],
lists:foreach( lists:foreach(
fun(User) -> fun(User) ->
{ok, 201, UserData} = request(post, UsersUri, User), {ok, 201, UserData} = request(post, UsersUri, User),
CreatedUser = jiffy:decode(UserData, [return_maps]), CreatedUser = jiffy:decode(UserData, [return_maps]),
?assertMatch(#{<<"user_id">> := _}, CreatedUser) ?assertMatch(#{<<"user_id">> := _}, CreatedUser)
end, end,
ValidUsers), ValidUsers
),
{ok, 200, Page1Data} = request(get, UsersUri ++ "?page=1&limit=2"), {ok, 200, Page1Data} = request(get, UsersUri ++ "?page=1&limit=2"),
#{<<"data">> := Page1Users, #{
<<"meta">> := <<"data">> := Page1Users,
#{<<"page">> := 1, <<"meta">> :=
<<"limit">> := 2, #{
<<"count">> := 3}} = <<"page">> := 1,
jiffy:decode(Page1Data, [return_maps]), <<"limit">> := 2,
<<"count">> := 3
}
} =
jiffy:decode(Page1Data, [return_maps]),
{ok, 200, Page2Data} = request(get, UsersUri ++ "?page=2&limit=2"), {ok, 200, Page2Data} = request(get, UsersUri ++ "?page=2&limit=2"),
#{<<"data">> := Page2Users, #{
<<"meta">> := <<"data">> := Page2Users,
#{<<"page">> := 2, <<"meta">> :=
<<"limit">> := 2, #{
<<"count">> := 3}} = jiffy:decode(Page2Data, [return_maps]), <<"page">> := 2,
<<"limit">> := 2,
<<"count">> := 3
}
} = jiffy:decode(Page2Data, [return_maps]),
?assertEqual(2, length(Page1Users)), ?assertEqual(2, length(Page1Users)),
?assertEqual(1, length(Page2Users)), ?assertEqual(1, length(Page2Users)),
?assertEqual( ?assertEqual(
[<<"u1">>, <<"u2">>, <<"u3">>], [<<"u1">>, <<"u2">>, <<"u3">>],
lists:usort([ UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])). lists:usort([UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])
).
test_authenticator_user(PathPrefix) -> test_authenticator_user(PathPrefix) ->
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]), UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
{ok, 200, _} = request( {ok, 200, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
emqx_authn_test_lib:built_in_database_example()), emqx_authn_test_lib:built_in_database_example()
),
User = #{user_id => <<"u1">>, password => <<"p1">>}, User = #{user_id => <<"u1">>, password => <<"p1">>},
{ok, 201, _} = request(post, UsersUri, User), {ok, 201, _} = request(post, UsersUri, User),
@ -299,141 +340,161 @@ test_authenticator_user(PathPrefix) ->
?assertNotMatch(#{<<"password">> := _}, FetchedUser), ?assertNotMatch(#{<<"password">> := _}, FetchedUser),
ValidUserUpdates = [ ValidUserUpdates = [
#{password => <<"p1">>}, #{password => <<"p1">>},
#{password => <<"p1">>, is_superuser => true}], #{password => <<"p1">>, is_superuser => true}
],
lists:foreach( lists:foreach(
fun(UserUpdate) -> {ok, 200, _} = request(put, UsersUri ++ "/u1", UserUpdate) end, fun(UserUpdate) -> {ok, 200, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
ValidUserUpdates), ValidUserUpdates
),
InvalidUserUpdates = [#{user_id => <<"u1">>, password => <<"p1">>}], InvalidUserUpdates = [#{user_id => <<"u1">>, password => <<"p1">>}],
lists:foreach( lists:foreach(
fun(UserUpdate) -> {ok, 400, _} = request(put, UsersUri ++ "/u1", UserUpdate) end, fun(UserUpdate) -> {ok, 400, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
InvalidUserUpdates), InvalidUserUpdates
),
{ok, 404, _} = request(delete, UsersUri ++ "/u123"), {ok, 404, _} = request(delete, UsersUri ++ "/u123"),
{ok, 204, _} = request(delete, UsersUri ++ "/u1"). {ok, 204, _} = request(delete, UsersUri ++ "/u1").
test_authenticator_move(PathPrefix) -> test_authenticator_move(PathPrefix) ->
AuthenticatorConfs = [ AuthenticatorConfs = [
emqx_authn_test_lib:http_example(), emqx_authn_test_lib:http_example(),
emqx_authn_test_lib:jwt_example(), emqx_authn_test_lib:jwt_example(),
emqx_authn_test_lib:built_in_database_example() emqx_authn_test_lib:built_in_database_example()
], ],
lists:foreach( lists:foreach(
fun(Conf) -> fun(Conf) ->
{ok, 200, _} = request( {ok, 200, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
Conf) Conf
end, )
AuthenticatorConfs), end,
AuthenticatorConfs
),
?assertAuthenticatorsMatch( ?assertAuthenticatorsMatch(
[ [
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}, #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
#{<<"mechanism">> := <<"jwt">>}, #{<<"mechanism">> := <<"jwt">>},
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>} #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
], ],
PathPrefix ++ [?CONF_NS]), PathPrefix ++ [?CONF_NS]
),
%% Invalid moves %% Invalid moves
{ok, 400, _} = request( {ok, 400, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]), uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
#{position => <<"up">>}), #{position => <<"up">>}
),
{ok, 400, _} = request( {ok, 400, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]), uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
#{}), #{}
),
{ok, 404, _} = request( {ok, 404, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]), uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
#{position => <<"before:invalid">>}), #{position => <<"before:invalid">>}
),
{ok, 404, _} = request( {ok, 404, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]), uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
#{position => <<"before:password_based:redis">>}), #{position => <<"before:password_based:redis">>}
),
{ok, 404, _} = request( {ok, 404, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]), uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
#{position => <<"before:password_based:redis">>}), #{position => <<"before:password_based:redis">>}
),
%% Valid moves %% Valid moves
%% test front %% test front
{ok, 204, _} = request( {ok, 204, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]), uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
#{position => <<"front">>}), #{position => <<"front">>}
),
?assertAuthenticatorsMatch( ?assertAuthenticatorsMatch(
[ [
#{<<"mechanism">> := <<"jwt">>}, #{<<"mechanism">> := <<"jwt">>},
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}, #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>} #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
], ],
PathPrefix ++ [?CONF_NS]), PathPrefix ++ [?CONF_NS]
),
%% test rear %% test rear
{ok, 204, _} = request( {ok, 204, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]), uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
#{position => <<"rear">>}), #{position => <<"rear">>}
),
?assertAuthenticatorsMatch( ?assertAuthenticatorsMatch(
[ [
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}, #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}, #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
#{<<"mechanism">> := <<"jwt">>} #{<<"mechanism">> := <<"jwt">>}
], ],
PathPrefix ++ [?CONF_NS]), PathPrefix ++ [?CONF_NS]
),
%% test before %% test before
{ok, 204, _} = request( {ok, 204, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]), uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
#{position => <<"before:password_based:built_in_database">>}), #{position => <<"before:password_based:built_in_database">>}
),
?assertAuthenticatorsMatch( ?assertAuthenticatorsMatch(
[ [
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}, #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
#{<<"mechanism">> := <<"jwt">>}, #{<<"mechanism">> := <<"jwt">>},
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>} #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
], ],
PathPrefix ++ [?CONF_NS]), PathPrefix ++ [?CONF_NS]
),
%% test after %% test after
{ok, 204, _} = request( {ok, 204, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS, "password_based%3Abuilt_in_database", "move"]), uri(PathPrefix ++ [?CONF_NS, "password_based%3Abuilt_in_database", "move"]),
#{position => <<"after:password_based:http">>}), #{position => <<"after:password_based:http">>}
),
?assertAuthenticatorsMatch( ?assertAuthenticatorsMatch(
[ [
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}, #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}, #{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
#{<<"mechanism">> := <<"jwt">>} #{<<"mechanism">> := <<"jwt">>}
], ],
PathPrefix ++ [?CONF_NS]). PathPrefix ++ [?CONF_NS]
).
test_authenticator_import_users(PathPrefix) -> test_authenticator_import_users(PathPrefix) ->
ImportUri = uri( ImportUri = uri(
PathPrefix ++ PathPrefix ++
[?CONF_NS, "password_based:built_in_database", "import_users"]), [?CONF_NS, "password_based:built_in_database", "import_users"]
),
{ok, 200, _} = request( {ok, 200, _} = request(
post, post,
uri(PathPrefix ++ [?CONF_NS]), uri(PathPrefix ++ [?CONF_NS]),
emqx_authn_test_lib:built_in_database_example()), emqx_authn_test_lib:built_in_database_example()
),
{ok, 400, _} = request(post, ImportUri, #{}), {ok, 400, _} = request(post, ImportUri, #{}),

View File

@ -28,12 +28,12 @@
-define(HTTP_PORT, 33333). -define(HTTP_PORT, 33333).
-define(HTTP_PATH, "/auth"). -define(HTTP_PATH, "/auth").
-define(CREDENTIALS, #{username => <<"plain">>, -define(CREDENTIALS, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}). protocol => mqtt
}).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -46,8 +46,9 @@ init_per_suite(Config) ->
end_per_suite(_) -> end_per_suite(_) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
emqx_common_test_helpers:stop_apps([emqx_authn]), emqx_common_test_helpers:stop_apps([emqx_authn]),
application:stop(cowboy), application:stop(cowboy),
ok. ok.
@ -55,8 +56,9 @@ end_per_suite(_) ->
init_per_testcase(_Case, Config) -> init_per_testcase(_Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
{ok, _} = emqx_authn_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH), {ok, _} = emqx_authn_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH),
Config. Config.
@ -71,8 +73,9 @@ t_create(_Config) ->
AuthConfig = raw_http_auth_config(), AuthConfig = raw_http_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_http}]} = emqx_authentication:list_authenticators(?GLOBAL). {ok, [#{provider := emqx_authn_http}]} = emqx_authentication:list_authenticators(?GLOBAL).
@ -81,83 +84,96 @@ t_create_invalid(_Config) ->
InvalidConfigs = InvalidConfigs =
[ [
AuthConfig#{headers => []}, AuthConfig#{headers => []},
AuthConfig#{method => delete} AuthConfig#{method => delete}
], ],
lists:foreach( lists:foreach(
fun(Config) -> fun(Config) ->
ct:pal("creating authenticator with invalid config: ~p", [Config]), ct:pal("creating authenticator with invalid config: ~p", [Config]),
{error, _} = {error, _} =
try try
emqx:update_config( emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}) {create_authenticator, ?GLOBAL, Config}
catch )
throw:Error -> catch
{error, Error} throw:Error ->
end, {error, Error}
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL) end,
end, {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
InvalidConfigs). end,
InvalidConfigs
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = lists:foreach( ok = lists:foreach(
fun(Sample) -> fun(Sample) ->
ct:pal("test_user_auth sample: ~p", [Sample]), ct:pal("test_user_auth sample: ~p", [Sample]),
test_user_auth(Sample) test_user_auth(Sample)
end, end,
samples()). samples()
).
test_user_auth(#{handler := Handler, test_user_auth(#{
config_params := SpecificConfgParams, handler := Handler,
result := Result}) -> config_params := SpecificConfgParams,
result := Result
}) ->
AuthConfig = maps:merge(raw_http_auth_config(), SpecificConfgParams), AuthConfig = maps:merge(raw_http_auth_config(), SpecificConfgParams),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
ok = emqx_authn_http_test_server:set_handler(Handler), ok = emqx_authn_http_test_server:set_handler(Handler),
?assertEqual(Result, emqx_access_control:authenticate(?CREDENTIALS)), ?assertEqual(Result, emqx_access_control:authenticate(?CREDENTIALS)),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL). ?GLOBAL
).
t_destroy(_Config) -> t_destroy(_Config) ->
AuthConfig = raw_http_auth_config(), AuthConfig = raw_http_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
ok = emqx_authn_http_test_server:set_handler( ok = emqx_authn_http_test_server:set_handler(
fun(Req0, State) -> fun(Req0, State) ->
Req = cowboy_req:reply(200, Req0), Req = cowboy_req:reply(200, Req0),
{ok, Req, State} {ok, Req, State}
end), end
),
{ok, [#{provider := emqx_authn_http, state := State}]} {ok, [#{provider := emqx_authn_http, state := State}]} =
= emqx_authentication:list_authenticators(?GLOBAL), emqx_authentication:list_authenticators(?GLOBAL),
Credentials = maps:with([username, password], ?CREDENTIALS), Credentials = maps:with([username, password], ?CREDENTIALS),
{ok, _} = emqx_authn_http:authenticate( {ok, _} = emqx_authn_http:authenticate(
Credentials, Credentials,
State), State
),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
% Authenticator should not be usable anymore % Authenticator should not be usable anymore
?assertMatch( ?assertMatch(
ignore, ignore,
emqx_authn_http:authenticate( emqx_authn_http:authenticate(
Credentials, Credentials,
State)). State
)
).
t_update(_Config) -> t_update(_Config) ->
CorrectConfig = raw_http_auth_config(), CorrectConfig = raw_http_auth_config(),
@ -165,74 +181,80 @@ t_update(_Config) ->
CorrectConfig#{url => <<"http://127.0.0.1:33333/invalid">>}, CorrectConfig#{url => <<"http://127.0.0.1:33333/invalid">>},
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, IncorrectConfig}), {create_authenticator, ?GLOBAL, IncorrectConfig}
),
ok = emqx_authn_http_test_server:set_handler( ok = emqx_authn_http_test_server:set_handler(
fun(Req0, State) -> fun(Req0, State) ->
Req = cowboy_req:reply(200, Req0), Req = cowboy_req:reply(200, Req0),
{ok, Req, State} {ok, Req, State}
end), end
),
{error, not_authorized} = emqx_access_control:authenticate(?CREDENTIALS), {error, not_authorized} = emqx_access_control:authenticate(?CREDENTIALS),
% We update with config with correct query, provider should update and work properly % We update with config with correct query, provider should update and work properly
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{update_authenticator, ?GLOBAL, <<"password_based:http">>, CorrectConfig}), {update_authenticator, ?GLOBAL, <<"password_based:http">>, CorrectConfig}
),
{ok,_} = emqx_access_control:authenticate(?CREDENTIALS). {ok, _} = emqx_access_control:authenticate(?CREDENTIALS).
t_is_superuser(_Config) -> t_is_superuser(_Config) ->
Config = raw_http_auth_config(), Config = raw_http_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
),
Checks = [ Checks = [
{json, <<"0">>, false}, {json, <<"0">>, false},
{json, <<"">>, false}, {json, <<"">>, false},
{json, null, false}, {json, null, false},
{json, 0, false}, {json, 0, false},
{json, <<"1">>, true}, {json, <<"1">>, true},
{json, <<"val">>, true}, {json, <<"val">>, true},
{json, 1, true}, {json, 1, true},
{json, 123, true}, {json, 123, true},
{form, <<"0">>, false}, {form, <<"0">>, false},
{form, <<"">>, false}, {form, <<"">>, false},
{form, <<"1">>, true}, {form, <<"1">>, true},
{form, <<"val">>, true} {form, <<"val">>, true}
], ],
lists:foreach(fun test_is_superuser/1, Checks). lists:foreach(fun test_is_superuser/1, Checks).
test_is_superuser({Kind, Value, ExpectedValue}) -> test_is_superuser({Kind, Value, ExpectedValue}) ->
{ContentType, Res} =
{ContentType, Res} = case Kind of case Kind of
json -> json ->
{<<"application/json">>, {<<"application/json">>, jiffy:encode(#{is_superuser => Value})};
jiffy:encode(#{is_superuser => Value})}; form ->
form -> {<<"application/x-www-form-urlencoded">>,
{<<"application/x-www-form-urlencoded">>, iolist_to_binary([<<"is_superuser=">>, Value])}
iolist_to_binary([<<"is_superuser=">>, Value])} end,
end,
ok = emqx_authn_http_test_server:set_handler( ok = emqx_authn_http_test_server:set_handler(
fun(Req0, State) -> fun(Req0, State) ->
Req = cowboy_req:reply( Req = cowboy_req:reply(
200, 200,
#{<<"content-type">> => ContentType}, #{<<"content-type">> => ContentType},
Res, Res,
Req0), Req0
{ok, Req, State} ),
end), {ok, Req, State}
end
),
?assertMatch( ?assertMatch(
{ok, #{is_superuser := ExpectedValue}}, {ok, #{is_superuser := ExpectedValue}},
emqx_access_control:authenticate(?CREDENTIALS)). emqx_access_control:authenticate(?CREDENTIALS)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -252,138 +274,159 @@ raw_http_auth_config() ->
samples() -> samples() ->
[ [
%% simple get request %% simple get request
#{handler => fun(Req0, State) -> #{
#{username := <<"plain">>, handler => fun(Req0, State) ->
password := <<"plain">> #{
} = cowboy_req:match_qs([username, password], Req0), username := <<"plain">>,
password := <<"plain">>
} = cowboy_req:match_qs([username, password], Req0),
Req = cowboy_req:reply(200, Req0), Req = cowboy_req:reply(200, Req0),
{ok, Req, State} {ok, Req, State}
end, end,
config_params => #{}, config_params => #{},
result => {ok,#{is_superuser => false}} result => {ok, #{is_superuser => false}}
}, },
%% get request with json body response %% get request with json body response
#{handler => fun(Req0, State) -> #{
Req = cowboy_req:reply( handler => fun(Req0, State) ->
200, Req = cowboy_req:reply(
#{<<"content-type">> => <<"application/json">>}, 200,
jiffy:encode(#{is_superuser => true}), #{<<"content-type">> => <<"application/json">>},
Req0), jiffy:encode(#{is_superuser => true}),
{ok, Req, State} Req0
end, ),
config_params => #{}, {ok, Req, State}
result => {ok,#{is_superuser => true, user_property => #{}}} end,
}, config_params => #{},
result => {ok, #{is_superuser => true, user_property => #{}}}
},
%% get request with url-form-encoded body response %% get request with url-form-encoded body response
#{handler => fun(Req0, State) -> #{
Req = cowboy_req:reply( handler => fun(Req0, State) ->
200, Req = cowboy_req:reply(
#{<<"content-type">> => 200,
<<"application/x-www-form-urlencoded">>}, #{
<<"is_superuser=true">>, <<"content-type">> =>
Req0), <<"application/x-www-form-urlencoded">>
{ok, Req, State} },
end, <<"is_superuser=true">>,
config_params => #{}, Req0
result => {ok,#{is_superuser => true, user_property => #{}}} ),
}, {ok, Req, State}
end,
config_params => #{},
result => {ok, #{is_superuser => true, user_property => #{}}}
},
%% get request with response of unknown encoding %% get request with response of unknown encoding
#{handler => fun(Req0, State) -> #{
Req = cowboy_req:reply( handler => fun(Req0, State) ->
200, Req = cowboy_req:reply(
#{<<"content-type">> => 200,
<<"test/plain">>}, #{
<<"is_superuser=true">>, <<"content-type">> =>
Req0), <<"test/plain">>
{ok, Req, State} },
end, <<"is_superuser=true">>,
config_params => #{}, Req0
result => {ok,#{is_superuser => false}} ),
}, {ok, Req, State}
end,
config_params => #{},
result => {ok, #{is_superuser => false}}
},
%% simple post request, application/json %% simple post request, application/json
#{handler => fun(Req0, State) -> #{
{ok, RawBody, Req1} = cowboy_req:read_body(Req0), handler => fun(Req0, State) ->
#{<<"username">> := <<"plain">>, {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
<<"password">> := <<"plain">> #{
} = jiffy:decode(RawBody, [return_maps]), <<"username">> := <<"plain">>,
Req = cowboy_req:reply(200, Req1), <<"password">> := <<"plain">>
{ok, Req, State} } = jiffy:decode(RawBody, [return_maps]),
end, Req = cowboy_req:reply(200, Req1),
config_params => #{ {ok, Req, State}
method => post, end,
headers => #{<<"content-type">> => <<"application/json">>} config_params => #{
}, method => post,
result => {ok,#{is_superuser => false}} headers => #{<<"content-type">> => <<"application/json">>}
}, },
result => {ok, #{is_superuser => false}}
},
%% simple post request, application/x-www-form-urlencoded %% simple post request, application/x-www-form-urlencoded
#{handler => fun(Req0, State) -> #{
{ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0), handler => fun(Req0, State) ->
#{<<"username">> := <<"plain">>, {ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0),
<<"password">> := <<"plain">> #{
} = maps:from_list(PostVars), <<"username">> := <<"plain">>,
Req = cowboy_req:reply(200, Req1), <<"password">> := <<"plain">>
{ok, Req, State} } = maps:from_list(PostVars),
end, Req = cowboy_req:reply(200, Req1),
config_params => #{ {ok, Req, State}
method => post, end,
headers => #{<<"content-type">> => config_params => #{
<<"application/x-www-form-urlencoded">>} method => post,
}, headers => #{
result => {ok,#{is_superuser => false}} <<"content-type">> =>
} <<"application/x-www-form-urlencoded">>
}
},
result => {ok, #{is_superuser => false}}
}#{
%% 204 code
handler => fun(Req0, State) ->
Req = cowboy_req:reply(204, Req0),
{ok, Req, State}
end,
config_params => #{},
result => {ok, #{is_superuser => false}}
},
%% 204 code %% custom headers
#{handler => fun(Req0, State) -> #{
Req = cowboy_req:reply(204, Req0), handler => fun(Req0, State) ->
{ok, Req, State} <<"Test Value">> = cowboy_req:header(<<"x-test-header">>, Req0),
end, Req = cowboy_req:reply(200, Req0),
config_params => #{}, {ok, Req, State}
result => {ok,#{is_superuser => false}} end,
}, config_params => #{},
result => {ok, #{is_superuser => false}}
},
%% custom headers %% 400 code
#{handler => fun(Req0, State) -> #{
<<"Test Value">> = cowboy_req:header(<<"x-test-header">>, Req0), handler => fun(Req0, State) ->
Req = cowboy_req:reply(200, Req0), Req = cowboy_req:reply(400, Req0),
{ok, Req, State} {ok, Req, State}
end, end,
config_params => #{}, config_params => #{},
result => {ok,#{is_superuser => false}} result => {error, not_authorized}
}, },
%% 400 code %% 500 code
#{handler => fun(Req0, State) -> #{
Req = cowboy_req:reply(400, Req0), handler => fun(Req0, State) ->
{ok, Req, State} Req = cowboy_req:reply(500, Req0),
end, {ok, Req, State}
config_params => #{}, end,
result => {error,not_authorized} config_params => #{},
}, result => {error, not_authorized}
},
%% 500 code %% Handling error
#{handler => fun(Req0, State) -> #{
Req = cowboy_req:reply(500, Req0), handler => fun(Req0, State) ->
{ok, Req, State} error(woops),
end, {ok, Req0, State}
config_params => #{}, end,
result => {error,not_authorized} config_params => #{},
}, result => {error, not_authorized}
}
%% Handling error
#{handler => fun(Req0, State) ->
error(woops),
{ok, Req0, State}
end,
config_params => #{},
result => {error,not_authorized}
}
]. ].
start_apps(Apps) -> start_apps(Apps) ->

View File

@ -26,11 +26,12 @@
-export([init/1]). -export([init/1]).
% API % API
-export([start_link/2, -export([
start_link/3, start_link/2,
stop/0, start_link/3,
set_handler/1 stop/0,
]). set_handler/1
]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% API %% API
@ -55,9 +56,10 @@ set_handler(F) when is_function(F, 2) ->
init([Port, Path, SSLOpts]) -> init([Port, Path, SSLOpts]) ->
Dispatch = cowboy_router:compile( Dispatch = cowboy_router:compile(
[ [
{'_', [{Path, ?MODULE, []}]} {'_', [{Path, ?MODULE, []}]}
]), ]
),
ProtoOpts = #{env => #{dispatch => Dispatch}}, ProtoOpts = #{env => #{dispatch => Dispatch}},
@ -83,23 +85,28 @@ init(Req, State) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
transport_settings(Port, false) -> transport_settings(Port, false) ->
TransOpts = #{socket_opts => [{port, Port}], TransOpts = #{
connection_type => supervisor}, socket_opts => [{port, Port}],
connection_type => supervisor
},
{ranch_tcp, TransOpts, cowboy_clear}; {ranch_tcp, TransOpts, cowboy_clear};
transport_settings(Port, SSLOpts) -> transport_settings(Port, SSLOpts) ->
TransOpts = #{socket_opts => [{port, Port}, TransOpts = #{
{next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]}, socket_opts => [
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]} {port, Port},
| SSLOpts], {next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
connection_type => supervisor}, {alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
| SSLOpts
],
connection_type => supervisor
},
{ranch_ssl, TransOpts, cowboy_tls}. {ranch_ssl, TransOpts, cowboy_tls}.
default_handler(Req0, State) -> default_handler(Req0, State) ->
Req = cowboy_req:reply( Req = cowboy_req:reply(
400, 400,
#{<<"content-type">> => <<"text/plain">>}, #{<<"content-type">> => <<"text/plain">>},
<<"">>, <<"">>,
Req0), Req0
),
{ok, Req, State}. {ok, Req, State}.

View File

@ -28,12 +28,12 @@
-define(HTTPS_PORT, 33333). -define(HTTPS_PORT, 33333).
-define(HTTPS_PATH, "/auth"). -define(HTTPS_PATH, "/auth").
-define(CREDENTIALS, #{username => <<"plain">>, -define(CREDENTIALS, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}). protocol => mqtt
}).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -46,8 +46,9 @@ init_per_suite(Config) ->
end_per_suite(_) -> end_per_suite(_) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
emqx_common_test_helpers:stop_apps([emqx_authn]), emqx_common_test_helpers:stop_apps([emqx_authn]),
application:stop(cowboy), application:stop(cowboy),
ok. ok.
@ -55,8 +56,9 @@ end_per_suite(_) ->
init_per_testcase(_Case, Config) -> init_per_testcase(_Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
{ok, _} = emqx_authn_http_test_server:start_link(?HTTPS_PORT, ?HTTPS_PATH, server_ssl_opts()), {ok, _} = emqx_authn_http_test_server:start_link(?HTTPS_PORT, ?HTTPS_PATH, server_ssl_opts()),
ok = emqx_authn_http_test_server:set_handler(fun cowboy_handler/2), ok = emqx_authn_http_test_server:set_handler(fun cowboy_handler/2),
Config. Config.
@ -70,46 +72,62 @@ end_per_testcase(_Case, _Config) ->
t_create(_Config) -> t_create(_Config) ->
{ok, _} = create_https_auth_with_ssl_opts( {ok, _} = create_https_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}), <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
}
),
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
emqx_access_control:authenticate(?CREDENTIALS)). emqx_access_control:authenticate(?CREDENTIALS)
).
t_create_invalid_domain(_Config) -> t_create_invalid_domain(_Config) ->
{ok, _} = create_https_auth_with_ssl_opts( {ok, _} = create_https_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server-unknown-host">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}), <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
}
),
?assertEqual( ?assertEqual(
{error, not_authorized}, {error, not_authorized},
emqx_access_control:authenticate(?CREDENTIALS)). emqx_access_control:authenticate(?CREDENTIALS)
).
t_create_invalid_version(_Config) -> t_create_invalid_version(_Config) ->
{ok, _} = create_https_auth_with_ssl_opts( {ok, _} = create_https_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.1">>]}), <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>]
}
),
?assertEqual( ?assertEqual(
{error, not_authorized}, {error, not_authorized},
emqx_access_control:authenticate(?CREDENTIALS)). emqx_access_control:authenticate(?CREDENTIALS)
).
t_create_invalid_ciphers(_Config) -> t_create_invalid_ciphers(_Config) ->
{ok, _} = create_https_auth_with_ssl_opts( {ok, _} = create_https_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"ECDHE-ECDSA-AES256-SHA384">>]}), <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-ECDSA-AES256-SHA384">>]
}
),
?assertEqual( ?assertEqual(
{error, not_authorized}, {error, not_authorized},
emqx_access_control:authenticate(?CREDENTIALS)). emqx_access_control:authenticate(?CREDENTIALS)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -121,8 +139,9 @@ create_https_auth_with_ssl_opts(SpecificSSLOpts) ->
raw_https_auth_config(SpecificSSLOpts) -> raw_https_auth_config(SpecificSSLOpts) ->
SSLOpts = maps:merge( SSLOpts = maps:merge(
emqx_authn_test_lib:client_ssl_cert_opts(), emqx_authn_test_lib:client_ssl_cert_opts(),
#{enable => <<"true">>}), #{enable => <<"true">>}
),
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
enable => <<"true">>, enable => <<"true">>,
@ -133,7 +152,7 @@ raw_https_auth_config(SpecificSSLOpts) ->
body => #{<<"username">> => ?PH_USERNAME, <<"password">> => ?PH_PASSWORD}, body => #{<<"username">> => ?PH_USERNAME, <<"password">> => ?PH_PASSWORD},
headers => #{<<"X-Test-Header">> => <<"Test Value">>}, headers => #{<<"X-Test-Header">> => <<"Test Value">>},
ssl => maps:merge(SSLOpts, SpecificSSLOpts) ssl => maps:merge(SSLOpts, SpecificSSLOpts)
}. }.
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).
@ -147,15 +166,17 @@ cert_path(FileName) ->
cowboy_handler(Req0, State) -> cowboy_handler(Req0, State) ->
Req = cowboy_req:reply( Req = cowboy_req:reply(
200, 200,
Req0), Req0
),
{ok, Req, State}. {ok, Req, State}.
server_ssl_opts() -> server_ssl_opts() ->
[{keyfile, cert_path("server.key")}, [
{certfile, cert_path("server.crt")}, {keyfile, cert_path("server.key")},
{cacertfile, cert_path("ca.crt")}, {certfile, cert_path("server.crt")},
{verify, verify_none}, {cacertfile, cert_path("ca.crt")},
{versions, ['tlsv1.2', 'tlsv1.3']}, {verify, verify_none},
{ciphers, ["ECDHE-RSA-AES256-GCM-SHA384", "TLS_CHACHA20_POLY1305_SHA256"]} {versions, ['tlsv1.2', 'tlsv1.3']},
{ciphers, ["ECDHE-RSA-AES256-GCM-SHA384", "TLS_CHACHA20_POLY1305_SHA256"]}
]. ].

View File

@ -28,7 +28,6 @@
-define(JWKS_PORT, 33333). -define(JWKS_PORT, 33333).
-define(JWKS_PATH, "/jwks.json"). -define(JWKS_PATH, "/jwks.json").
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -51,24 +50,30 @@ end_per_suite(_) ->
t_jwt_authenticator_hmac_based(_) -> t_jwt_authenticator_hmac_based(_) ->
Secret = <<"abcdef">>, Secret = <<"abcdef">>,
Config = #{mechanism => jwt, Config = #{
use_jwks => false, mechanism => jwt,
algorithm => 'hmac-based', use_jwks => false,
secret => Secret, algorithm => 'hmac-based',
secret_base64_encoded => false, secret => Secret,
verify_claims => []}, secret_base64_encoded => false,
verify_claims => []
},
{ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config), {ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config),
Payload = #{<<"username">> => <<"myuser">>}, Payload = #{<<"username">> => <<"myuser">>},
JWS = generate_jws('hmac-based', Payload, Secret), JWS = generate_jws('hmac-based', Payload, Secret),
Credential = #{username => <<"myuser">>, Credential = #{
password => JWS}, username => <<"myuser">>,
password => JWS
},
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)), ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true}, Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true},
JWS1 = generate_jws('hmac-based', Payload1, Secret), JWS1 = generate_jws('hmac-based', Payload1, Secret),
Credential1 = #{username => <<"myuser">>, Credential1 = #{
password => JWS1}, username => <<"myuser">>,
password => JWS1
},
?assertEqual({ok, #{is_superuser => true}}, emqx_authn_jwt:authenticate(Credential1, State)), ?assertEqual({ok, #{is_superuser => true}}, emqx_authn_jwt:authenticate(Credential1, State)),
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
@ -76,59 +81,84 @@ t_jwt_authenticator_hmac_based(_) ->
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential2, State)), ?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential2, State)),
%% secret_base64_encoded %% secret_base64_encoded
Config2 = Config#{secret => base64:encode(Secret), Config2 = Config#{
secret_base64_encoded => true}, secret => base64:encode(Secret),
secret_base64_encoded => true
},
{ok, State2} = emqx_authn_jwt:update(Config2, State), {ok, State2} = emqx_authn_jwt:update(Config2, State),
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)), ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)),
%% invalid secret %% invalid secret
BadConfig = Config#{secret => <<"emqxsecret">>, BadConfig = Config#{
secret_base64_encoded => true}, secret => <<"emqxsecret">>,
secret_base64_encoded => true
},
{error, {invalid_parameter, secret}} = emqx_authn_jwt:create(?AUTHN_ID, BadConfig), {error, {invalid_parameter, secret}} = emqx_authn_jwt:create(?AUTHN_ID, BadConfig),
Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]}, Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]},
{ok, State3} = emqx_authn_jwt:update(Config3, State2), {ok, State3} = emqx_authn_jwt:update(Config3, State2),
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State3)), ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State3)),
?assertEqual({error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential#{username => <<"otheruser">>}, State3)), ?assertEqual(
{error, bad_username_or_password},
emqx_authn_jwt:authenticate(Credential#{username => <<"otheruser">>}, State3)
),
%% Expiration %% Expiration
Payload3 = #{ <<"username">> => <<"myuser">> Payload3 = #{
, <<"exp">> => erlang:system_time(second) - 60}, <<"username">> => <<"myuser">>,
<<"exp">> => erlang:system_time(second) - 60
},
JWS3 = generate_jws('hmac-based', Payload3, Secret), JWS3 = generate_jws('hmac-based', Payload3, Secret),
Credential3 = Credential#{password => JWS3}, Credential3 = Credential#{password => JWS3},
?assertEqual({error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential3, State3)), ?assertEqual(
{error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential3, State3)
),
Payload4 = #{ <<"username">> => <<"myuser">> Payload4 = #{
, <<"exp">> => erlang:system_time(second) + 60}, <<"username">> => <<"myuser">>,
<<"exp">> => erlang:system_time(second) + 60
},
JWS4 = generate_jws('hmac-based', Payload4, Secret), JWS4 = generate_jws('hmac-based', Payload4, Secret),
Credential4 = Credential#{password => JWS4}, Credential4 = Credential#{password => JWS4},
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential4, State3)), ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential4, State3)),
%% Issued At %% Issued At
Payload5 = #{ <<"username">> => <<"myuser">> Payload5 = #{
, <<"iat">> => erlang:system_time(second) - 60}, <<"username">> => <<"myuser">>,
<<"iat">> => erlang:system_time(second) - 60
},
JWS5 = generate_jws('hmac-based', Payload5, Secret), JWS5 = generate_jws('hmac-based', Payload5, Secret),
Credential5 = Credential#{password => JWS5}, Credential5 = Credential#{password => JWS5},
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential5, State3)), ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential5, State3)),
Payload6 = #{ <<"username">> => <<"myuser">> Payload6 = #{
, <<"iat">> => erlang:system_time(second) + 60}, <<"username">> => <<"myuser">>,
<<"iat">> => erlang:system_time(second) + 60
},
JWS6 = generate_jws('hmac-based', Payload6, Secret), JWS6 = generate_jws('hmac-based', Payload6, Secret),
Credential6 = Credential#{password => JWS6}, Credential6 = Credential#{password => JWS6},
?assertEqual({error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential6, State3)), ?assertEqual(
{error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential6, State3)
),
%% Not Before %% Not Before
Payload7 = #{ <<"username">> => <<"myuser">> Payload7 = #{
, <<"nbf">> => erlang:system_time(second) - 60}, <<"username">> => <<"myuser">>,
<<"nbf">> => erlang:system_time(second) - 60
},
JWS7 = generate_jws('hmac-based', Payload7, Secret), JWS7 = generate_jws('hmac-based', Payload7, Secret),
Credential7 = Credential6#{password => JWS7}, Credential7 = Credential6#{password => JWS7},
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential7, State3)), ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential7, State3)),
Payload8 = #{ <<"username">> => <<"myuser">> Payload8 = #{
, <<"nbf">> => erlang:system_time(second) + 60}, <<"username">> => <<"myuser">>,
<<"nbf">> => erlang:system_time(second) + 60
},
JWS8 = generate_jws('hmac-based', Payload8, Secret), JWS8 = generate_jws('hmac-based', Payload8, Secret),
Credential8 = Credential#{password => JWS8}, Credential8 = Credential#{password => JWS8},
?assertEqual({error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential8, State3)), ?assertEqual(
{error, bad_username_or_password}, emqx_authn_jwt:authenticate(Credential8, State3)
),
?assertEqual(ok, emqx_authn_jwt:destroy(State3)), ?assertEqual(ok, emqx_authn_jwt:destroy(State3)),
ok. ok.
@ -136,19 +166,25 @@ t_jwt_authenticator_hmac_based(_) ->
t_jwt_authenticator_public_key(_) -> t_jwt_authenticator_public_key(_) ->
PublicKey = test_rsa_key(public), PublicKey = test_rsa_key(public),
PrivateKey = test_rsa_key(private), PrivateKey = test_rsa_key(private),
Config = #{mechanism => jwt, Config = #{
use_jwks => false, mechanism => jwt,
algorithm => 'public-key', use_jwks => false,
certificate => PublicKey, algorithm => 'public-key',
verify_claims => []}, certificate => PublicKey,
verify_claims => []
},
{ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config), {ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config),
Payload = #{<<"username">> => <<"myuser">>}, Payload = #{<<"username">> => <<"myuser">>},
JWS = generate_jws('public-key', Payload, PrivateKey), JWS = generate_jws('public-key', Payload, PrivateKey),
Credential = #{username => <<"myuser">>, Credential = #{
password => JWS}, username => <<"myuser">>,
password => JWS
},
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)), ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State)), ?assertEqual(
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State)
),
?assertEqual(ok, emqx_authn_jwt:destroy(State)), ?assertEqual(ok, emqx_authn_jwt:destroy(State)),
ok. ok.
@ -160,63 +196,77 @@ t_jwks_renewal(_Config) ->
PrivateKey = test_rsa_key(private), PrivateKey = test_rsa_key(private),
Payload = #{<<"username">> => <<"myuser">>}, Payload = #{<<"username">> => <<"myuser">>},
JWS = generate_jws('public-key', Payload, PrivateKey), JWS = generate_jws('public-key', Payload, PrivateKey),
Credential = #{username => <<"myuser">>, Credential = #{
password => JWS}, username => <<"myuser">>,
password => JWS
},
BadConfig0 = #{mechanism => jwt, BadConfig0 = #{
algorithm => 'public-key', mechanism => jwt,
ssl => #{enable => false}, algorithm => 'public-key',
verify_claims => [], ssl => #{enable => false},
verify_claims => [],
use_jwks => true, use_jwks => true,
endpoint => "https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT + 1) ++ ?JWKS_PATH, endpoint => "https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT + 1) ++ ?JWKS_PATH,
refresh_interval => 1000 refresh_interval => 1000
}, },
ok = snabbkaffe:start_trace(), ok = snabbkaffe:start_trace(),
{{ok, State0}, _} = ?wait_async_action( {{ok, State0}, _} = ?wait_async_action(
emqx_authn_jwt:create(?AUTHN_ID, BadConfig0), emqx_authn_jwt:create(?AUTHN_ID, BadConfig0),
#{?snk_kind := jwks_endpoint_response}, #{?snk_kind := jwks_endpoint_response},
10000), 10000
),
ok = snabbkaffe:stop(), ok = snabbkaffe:stop(),
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State0)), ?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State0)),
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)), ?assertEqual(
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)
),
ClientSSLOpts = client_ssl_opts(), ClientSSLOpts = client_ssl_opts(),
BadClientSSLOpts = ClientSSLOpts#{server_name_indication => "authn-server-unknown-host"}, BadClientSSLOpts = ClientSSLOpts#{server_name_indication => "authn-server-unknown-host"},
BadConfig1 = BadConfig0#{endpoint => BadConfig1 = BadConfig0#{
"https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH, endpoint =>
ssl => BadClientSSLOpts}, "https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH,
ssl => BadClientSSLOpts
},
ok = snabbkaffe:start_trace(), ok = snabbkaffe:start_trace(),
{{ok, State1}, _} = ?wait_async_action( {{ok, State1}, _} = ?wait_async_action(
emqx_authn_jwt:create(?AUTHN_ID, BadConfig1), emqx_authn_jwt:create(?AUTHN_ID, BadConfig1),
#{?snk_kind := jwks_endpoint_response}, #{?snk_kind := jwks_endpoint_response},
10000), 10000
),
ok = snabbkaffe:stop(), ok = snabbkaffe:stop(),
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State1)), ?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential, State1)),
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)), ?assertEqual(
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State0)
),
GoodConfig = BadConfig1#{ssl => ClientSSLOpts}, GoodConfig = BadConfig1#{ssl => ClientSSLOpts},
ok = snabbkaffe:start_trace(), ok = snabbkaffe:start_trace(),
{{ok, State2}, _} = ?wait_async_action( {{ok, State2}, _} = ?wait_async_action(
emqx_authn_jwt:update(GoodConfig, State1), emqx_authn_jwt:update(GoodConfig, State1),
#{?snk_kind := jwks_endpoint_response}, #{?snk_kind := jwks_endpoint_response},
10000), 10000
),
ok = snabbkaffe:stop(), ok = snabbkaffe:stop(),
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)), ?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)),
?assertEqual(ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State2)), ?assertEqual(
ignore, emqx_authn_jwt:authenticate(Credential#{password => <<"badpassword">>}, State2)
),
?assertEqual(ok, emqx_authn_jwt:destroy(State2)), ?assertEqual(ok, emqx_authn_jwt:destroy(State2)),
ok = emqx_authn_http_test_server:stop(). ok = emqx_authn_http_test_server:stop().
@ -229,15 +279,15 @@ jwks_handler(Req0, State) ->
JWK = jose_jwk:from_pem_file(test_rsa_key(public)), JWK = jose_jwk:from_pem_file(test_rsa_key(public)),
JWKS = jose_jwk_set:to_map([JWK], #{}), JWKS = jose_jwk_set:to_map([JWK], #{}),
Req = cowboy_req:reply( Req = cowboy_req:reply(
200, 200,
#{<<"content-type">> => <<"application/json">>}, #{<<"content-type">> => <<"application/json">>},
jiffy:encode(JWKS), jiffy:encode(JWKS),
Req0), Req0
),
{ok, Req, State}. {ok, Req, State}.
test_rsa_key(public) -> test_rsa_key(public) ->
data_file("public_key.pem"); data_file("public_key.pem");
test_rsa_key(private) -> test_rsa_key(private) ->
data_file("private_key.pem"). data_file("private_key.pem").
@ -250,32 +300,37 @@ cert_file(Name) ->
generate_jws('hmac-based', Payload, Secret) -> generate_jws('hmac-based', Payload, Secret) ->
JWK = jose_jwk:from_oct(Secret), JWK = jose_jwk:from_oct(Secret),
Header = #{ <<"alg">> => <<"HS256">> Header = #{
, <<"typ">> => <<"JWT">> <<"alg">> => <<"HS256">>,
}, <<"typ">> => <<"JWT">>
},
Signed = jose_jwt:sign(JWK, Header, Payload), Signed = jose_jwt:sign(JWK, Header, Payload),
{_, JWS} = jose_jws:compact(Signed), {_, JWS} = jose_jws:compact(Signed),
JWS; JWS;
generate_jws('public-key', Payload, PrivateKey) -> generate_jws('public-key', Payload, PrivateKey) ->
JWK = jose_jwk:from_pem_file(PrivateKey), JWK = jose_jwk:from_pem_file(PrivateKey),
Header = #{ <<"alg">> => <<"RS256">> Header = #{
, <<"typ">> => <<"JWT">> <<"alg">> => <<"RS256">>,
}, <<"typ">> => <<"JWT">>
},
Signed = jose_jwt:sign(JWK, Header, Payload), Signed = jose_jwt:sign(JWK, Header, Payload),
{_, JWS} = jose_jws:compact(Signed), {_, JWS} = jose_jws:compact(Signed),
JWS. JWS.
client_ssl_opts() -> client_ssl_opts() ->
maps:merge( maps:merge(
emqx_authn_test_lib:client_ssl_cert_opts(), emqx_authn_test_lib:client_ssl_cert_opts(),
#{enable => true, #{
verify => verify_peer, enable => true,
server_name_indication => "authn-server" verify => verify_peer,
}). server_name_indication => "authn-server"
}
).
server_ssl_opts() -> server_ssl_opts() ->
[{keyfile, cert_file("server.key")}, [
{certfile, cert_file("server.crt")}, {keyfile, cert_file("server.key")},
{cacertfile, cert_file("ca.crt")}, {certfile, cert_file("server.crt")},
{verify, verify_none} {cacertfile, cert_file("ca.crt")},
{verify, verify_none}
]. ].

View File

@ -76,7 +76,8 @@ t_check_schema(_Config) ->
?assertException( ?assertException(
throw, throw,
{emqx_authn_mnesia, _}, {emqx_authn_mnesia, _},
hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigNotOk))). hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigNotOk))
).
t_create(_) -> t_create(_) ->
Config0 = config(), Config0 = config(),
@ -110,7 +111,7 @@ t_destroy(_) ->
ok = emqx_authn_mnesia:destroy(State0), ok = emqx_authn_mnesia:destroy(State0),
{ok, State1} = emqx_authn_mnesia:create(?AUTHN_ID, Config), {ok, State1} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
{error,not_found} = emqx_authn_mnesia:lookup_user(<<"u">>, State1), {error, not_found} = emqx_authn_mnesia:lookup_user(<<"u">>, State1),
{ok, _} = emqx_authn_mnesia:lookup_user(<<"u">>, StateOther). {ok, _} = emqx_authn_mnesia:lookup_user(<<"u">>, StateOther).
t_authenticate(_) -> t_authenticate(_) ->
@ -121,14 +122,17 @@ t_authenticate(_) ->
{ok, _} = emqx_authn_mnesia:add_user(User, State), {ok, _} = emqx_authn_mnesia:add_user(User, State),
{ok, _} = emqx_authn_mnesia:authenticate( {ok, _} = emqx_authn_mnesia:authenticate(
#{username => <<"u">>, password => <<"p">>}, #{username => <<"u">>, password => <<"p">>},
State), State
),
{error, bad_username_or_password} = emqx_authn_mnesia:authenticate( {error, bad_username_or_password} = emqx_authn_mnesia:authenticate(
#{username => <<"u">>, password => <<"badpass">>}, #{username => <<"u">>, password => <<"badpass">>},
State), State
),
ignore = emqx_authn_mnesia:authenticate( ignore = emqx_authn_mnesia:authenticate(
#{clientid => <<"u">>, password => <<"p">>}, #{clientid => <<"u">>, password => <<"p">>},
State). State
).
t_add_user(_) -> t_add_user(_) ->
Config = config(), Config = config(),
@ -157,16 +161,19 @@ t_update_user(_) ->
{ok, _} = emqx_authn_mnesia:add_user(User, State), {ok, _} = emqx_authn_mnesia:add_user(User, State),
{error, not_found} = emqx_authn_mnesia:update_user(<<"u1">>, #{password => <<"p1">>}, State), {error, not_found} = emqx_authn_mnesia:update_user(<<"u1">>, #{password => <<"p1">>}, State),
{ok, {ok, #{
#{user_id := <<"u">>, user_id := <<"u">>,
is_superuser := true}} = emqx_authn_mnesia:update_user( is_superuser := true
<<"u">>, }} = emqx_authn_mnesia:update_user(
#{password => <<"p1">>, is_superuser => true}, <<"u">>,
State), #{password => <<"p1">>, is_superuser => true},
State
),
{ok, _} = emqx_authn_mnesia:authenticate( {ok, _} = emqx_authn_mnesia:authenticate(
#{username => <<"u">>, password => <<"p1">>}, #{username => <<"u">>, password => <<"p1">>},
State), State
),
{ok, #{is_superuser := true}} = emqx_authn_mnesia:lookup_user(<<"u">>, State). {ok, #{is_superuser := true}} = emqx_authn_mnesia:lookup_user(<<"u">>, State).
@ -174,31 +181,47 @@ t_list_users(_) ->
Config = config(), Config = config(),
{ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config), {ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
Users = [#{user_id => <<"u1">>, password => <<"p">>}, Users = [
#{user_id => <<"u2">>, password => <<"p">>}, #{user_id => <<"u1">>, password => <<"p">>},
#{user_id => <<"u3">>, password => <<"p">>}], #{user_id => <<"u2">>, password => <<"p">>},
#{user_id => <<"u3">>, password => <<"p">>}
],
lists:foreach( lists:foreach(
fun(U) -> {ok, _} = emqx_authn_mnesia:add_user(U, State) end, fun(U) -> {ok, _} = emqx_authn_mnesia:add_user(U, State) end,
Users), Users
),
#{data := [#{is_superuser := false,user_id := _}, #{
#{is_superuser := false,user_id := _}], data := [
meta := #{page := 1, limit := 2, count := 3}} = emqx_authn_mnesia:list_users( #{is_superuser := false, user_id := _},
#{<<"page">> => 1, <<"limit">> => 2}, #{is_superuser := false, user_id := _}
State), ],
meta := #{page := 1, limit := 2, count := 3}
} = emqx_authn_mnesia:list_users(
#{<<"page">> => 1, <<"limit">> => 2},
State
),
#{data := [#{is_superuser := false,user_id := _}], #{
meta := #{page := 2, limit := 2, count := 3}} = emqx_authn_mnesia:list_users( data := [#{is_superuser := false, user_id := _}],
#{<<"page">> => 2, <<"limit">> => 2}, meta := #{page := 2, limit := 2, count := 3}
State), } = emqx_authn_mnesia:list_users(
#{<<"page">> => 2, <<"limit">> => 2},
State
),
#{data := [#{is_superuser := false,user_id := <<"u3">>}], #{
meta := #{page := 1, limit := 20, count := 1}} = emqx_authn_mnesia:list_users( data := [#{is_superuser := false, user_id := <<"u3">>}],
#{ <<"page">> => 1 meta := #{page := 1, limit := 20, count := 1}
, <<"limit">> => 20 } = emqx_authn_mnesia:list_users(
, <<"like_username">> => <<"3">>}, #{
State). <<"page">> => 1,
<<"limit">> => 20,
<<"like_username">> => <<"3">>
},
State
).
t_import_users(_) -> t_import_users(_) ->
Config0 = config(), Config0 = config(),
@ -206,36 +229,44 @@ t_import_users(_) ->
{ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config), {ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
ok = emqx_authn_mnesia:import_users( ok = emqx_authn_mnesia:import_users(
data_filename(<<"user-credentials.json">>), data_filename(<<"user-credentials.json">>),
State), State
),
ok = emqx_authn_mnesia:import_users( ok = emqx_authn_mnesia:import_users(
data_filename(<<"user-credentials.csv">>), data_filename(<<"user-credentials.csv">>),
State), State
),
{error, {unsupported_file_format, _}} = emqx_authn_mnesia:import_users( {error, {unsupported_file_format, _}} = emqx_authn_mnesia:import_users(
<<"/file/with/unknown.extension">>, <<"/file/with/unknown.extension">>,
State), State
),
{error, unknown_file_format} = emqx_authn_mnesia:import_users( {error, unknown_file_format} = emqx_authn_mnesia:import_users(
<<"/file/with/no/extension">>, <<"/file/with/no/extension">>,
State), State
),
{error, enoent} = emqx_authn_mnesia:import_users( {error, enoent} = emqx_authn_mnesia:import_users(
<<"/file/that/not/exist.json">>, <<"/file/that/not/exist.json">>,
State), State
),
{error, bad_format} = emqx_authn_mnesia:import_users( {error, bad_format} = emqx_authn_mnesia:import_users(
data_filename(<<"user-credentials-malformed-0.json">>), data_filename(<<"user-credentials-malformed-0.json">>),
State), State
),
{error, {_, invalid_json}} = emqx_authn_mnesia:import_users( {error, {_, invalid_json}} = emqx_authn_mnesia:import_users(
data_filename(<<"user-credentials-malformed-1.json">>), data_filename(<<"user-credentials-malformed-1.json">>),
State), State
),
{error, bad_format} = emqx_authn_mnesia:import_users( {error, bad_format} = emqx_authn_mnesia:import_users(
data_filename(<<"user-credentials-malformed.csv">>), data_filename(<<"user-credentials-malformed.csv">>),
State). State
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -246,7 +277,10 @@ data_filename(Name) ->
filename:join([Dir, <<"data">>, Name]). filename:join([Dir, <<"data">>, Name]).
config() -> config() ->
#{user_id_type => username, #{
password_hash_algorithm => #{name => bcrypt, user_id_type => username,
salt_rounds => 8} password_hash_algorithm => #{
}. name => bcrypt,
salt_rounds => 8
}
}.

View File

@ -24,7 +24,6 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-define(MONGO_HOST, "mongo"). -define(MONGO_HOST, "mongo").
-define(MONGO_CLIENT, 'emqx_authn_mongo_SUITE_client'). -define(MONGO_CLIENT, 'emqx_authn_mongo_SUITE_client').
@ -37,8 +36,9 @@ init_per_testcase(_TestCase, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authentication:initialize_authentication(?GLOBAL, []), emqx_authentication:initialize_authentication(?GLOBAL, []),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
{ok, _} = mc_worker_api:connect(mongo_config()), {ok, _} = mc_worker_api:connect(mongo_config()),
Config. Config.
@ -58,8 +58,9 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
@ -71,8 +72,9 @@ t_create(_Config) ->
AuthConfig = raw_mongo_auth_config(), AuthConfig = raw_mongo_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_mongodb}]} = emqx_authentication:list_authenticators(?GLOBAL). {ok, [#{provider := emqx_authn_mongodb}]} = emqx_authentication:list_authenticators(?GLOBAL).
@ -81,79 +83,93 @@ t_create_invalid(_Config) ->
InvalidConfigs = InvalidConfigs =
[ [
AuthConfig#{mongo_type => <<"unknown">>}, AuthConfig#{mongo_type => <<"unknown">>},
AuthConfig#{selector => <<"{ \"username\": \"${username}\" }">>}, AuthConfig#{selector => <<"{ \"username\": \"${username}\" }">>},
AuthConfig#{w_mode => <<"unknown">>} AuthConfig#{w_mode => <<"unknown">>}
], ],
lists:foreach( lists:foreach(
fun(Config) -> fun(Config) ->
{error, _} = emqx:update_config( {error, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
),
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL) {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
end, end,
InvalidConfigs). InvalidConfigs
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = init_seeds(), ok = init_seeds(),
ok = lists:foreach( ok = lists:foreach(
fun(Sample) -> fun(Sample) ->
ct:pal("test_user_auth sample: ~p", [Sample]), ct:pal("test_user_auth sample: ~p", [Sample]),
test_user_auth(Sample) test_user_auth(Sample)
end, end,
user_seeds()), user_seeds()
),
ok = drop_seeds(). ok = drop_seeds().
test_user_auth(#{credentials := Credentials0, test_user_auth(#{
config_params := SpecificConfigParams, credentials := Credentials0,
result := Result}) -> config_params := SpecificConfigParams,
result := Result
}) ->
AuthConfig = maps:merge(raw_mongo_auth_config(), SpecificConfigParams), AuthConfig = maps:merge(raw_mongo_auth_config(), SpecificConfigParams),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
Credentials = Credentials0#{ Credentials = Credentials0#{
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt protocol => mqtt
}, },
?assertEqual(Result, emqx_access_control:authenticate(Credentials)), ?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL). ?GLOBAL
).
t_destroy(_Config) -> t_destroy(_Config) ->
ok = init_seeds(), ok = init_seeds(),
AuthConfig = raw_mongo_auth_config(), AuthConfig = raw_mongo_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_mongodb, state := State}]} {ok, [#{provider := emqx_authn_mongodb, state := State}]} =
= emqx_authentication:list_authenticators(?GLOBAL), emqx_authentication:list_authenticators(?GLOBAL),
{ok, _} = emqx_authn_mongodb:authenticate( {ok, _} = emqx_authn_mongodb:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">> username => <<"plain">>,
}, password => <<"plain">>
State), },
State
),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
% Authenticator should not be usable anymore % Authenticator should not be usable anymore
?assertMatch( ?assertMatch(
ignore, ignore,
emqx_authn_mongodb:authenticate( emqx_authn_mongodb:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">> username => <<"plain">>,
}, password => <<"plain">>
State)), },
State
)
),
ok = drop_seeds(). ok = drop_seeds().
@ -164,48 +180,55 @@ t_update(_Config) ->
CorrectConfig#{selector => #{<<"wrongfield">> => <<"wrongvalue">>}}, CorrectConfig#{selector => #{<<"wrongfield">> => <<"wrongvalue">>}},
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, IncorrectConfig}), {create_authenticator, ?GLOBAL, IncorrectConfig}
),
{error, not_authorized} = emqx_access_control:authenticate( {error, not_authorized} = emqx_access_control:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}), protocol => mqtt
}
),
% We update with config with correct selector, provider should update and work properly % We update with config with correct selector, provider should update and work properly
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{update_authenticator, ?GLOBAL, <<"password_based:mongodb">>, CorrectConfig}), {update_authenticator, ?GLOBAL, <<"password_based:mongodb">>, CorrectConfig}
),
{ok,_} = emqx_access_control:authenticate( {ok, _} = emqx_access_control:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}), protocol => mqtt
}
),
ok = drop_seeds(). ok = drop_seeds().
t_is_superuser(_Config) -> t_is_superuser(_Config) ->
Config = raw_mongo_auth_config(), Config = raw_mongo_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
),
Checks = [ Checks = [
{<<"0">>, false}, {<<"0">>, false},
{<<"">>, false}, {<<"">>, false},
{null, false}, {null, false},
{false, false}, {false, false},
{0, false}, {0, false},
{<<"1">>, true}, {<<"1">>, true},
{<<"val">>, true}, {<<"val">>, true},
{1, true}, {1, true},
{123, true}, {123, true},
{true, true} {true, true}
], ],
lists:foreach(fun test_is_superuser/1, Checks). lists:foreach(fun test_is_superuser/1, Checks).
@ -213,24 +236,25 @@ test_is_superuser({Value, ExpectedValue}) ->
{true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"users">>, #{}), {true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"users">>, #{}),
UserData = #{ UserData = #{
username => <<"user">>, username => <<"user">>,
password_hash => <<"plainsalt">>, password_hash => <<"plainsalt">>,
salt => <<"salt">>, salt => <<"salt">>,
is_superuser => Value is_superuser => Value
}, },
{{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, [UserData]), {{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, [UserData]),
Credentials = #{ Credentials = #{
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt, protocol => mqtt,
username => <<"user">>, username => <<"user">>,
password => <<"plain">> password => <<"plain">>
}, },
?assertEqual( ?assertEqual(
{ok, #{is_superuser => ExpectedValue}}, {ok, #{is_superuser => ExpectedValue}},
emqx_access_control:authenticate(Credentials)). emqx_access_control:authenticate(Credentials)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -238,146 +262,160 @@ test_is_superuser({Value, ExpectedValue}) ->
raw_mongo_auth_config() -> raw_mongo_auth_config() ->
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
password_hash_algorithm => #{name => <<"plain">>, password_hash_algorithm => #{
salt_position => <<"suffix">>}, name => <<"plain">>,
enable => <<"true">>, salt_position => <<"suffix">>
},
enable => <<"true">>,
backend => <<"mongodb">>, backend => <<"mongodb">>,
mongo_type => <<"single">>, mongo_type => <<"single">>,
database => <<"mqtt">>, database => <<"mqtt">>,
collection => <<"users">>, collection => <<"users">>,
server => mongo_server(), server => mongo_server(),
w_mode => <<"unsafe">>, w_mode => <<"unsafe">>,
selector => #{<<"username">> => <<"${username}">>}, selector => #{<<"username">> => <<"${username}">>},
password_hash_field => <<"password_hash">>, password_hash_field => <<"password_hash">>,
salt_field => <<"salt">>, salt_field => <<"salt">>,
is_superuser_field => <<"is_superuser">> is_superuser_field => <<"is_superuser">>
}. }.
user_seeds() -> user_seeds() ->
[#{data => #{ [
username => <<"plain">>, #{
password_hash => <<"plainsalt">>, data => #{
salt => <<"salt">>, username => <<"plain">>,
is_superuser => <<"1">> password_hash => <<"plainsalt">>,
}, salt => <<"salt">>,
credentials => #{ is_superuser => <<"1">>
username => <<"plain">>, },
password => <<"plain">> credentials => #{
}, username => <<"plain">>,
config_params => #{ password => <<"plain">>
}, },
result => {ok,#{is_superuser => true}} config_params => #{},
}, result => {ok, #{is_superuser => true}}
#{data => #{
username => <<"md5">>,
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"salt">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"md5">>,
password => <<"md5">>
},
config_params => #{
password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">> }
},
result => {ok,#{is_superuser => false}}
},
#{data => #{
username => <<"sha256">>,
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
salt => <<"salt">>,
is_superuser => 1
}, },
credentials => #{
clientid => <<"sha256">>,
password => <<"sha256">>
},
config_params => #{
selector => #{<<"username">> => <<"${clientid}">>},
password_hash_algorithm => #{name => <<"sha256">>,
salt_position => <<"prefix">>}
},
result => {ok,#{is_superuser => true}}
},
#{data => #{ #{
username => <<"bcrypt">>, data => #{
password_hash => username => <<"md5">>,
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, salt => <<"salt">>,
is_superuser => 0 is_superuser => <<"0">>
}, },
credentials => #{ credentials => #{
username => <<"bcrypt">>, username => <<"md5">>,
password => <<"bcrypt">> password => <<"md5">>
}, },
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{
}, name => <<"md5">>,
result => {ok,#{is_superuser => false}} salt_position => <<"suffix">>
}, }
},
result => {ok, #{is_superuser => false}}
},
#{data => #{ #{
username => <<"bcrypt0">>, data => #{
password_hash => username => <<"sha256">>,
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, password_hash =>
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
is_superuser => <<"0">> salt => <<"salt">>,
}, is_superuser => 1
credentials => #{ },
username => <<"bcrypt0">>, credentials => #{
password => <<"bcrypt">> clientid => <<"sha256">>,
}, password => <<"sha256">>
config_params => #{ },
% clientid variable & username credentials config_params => #{
selector => #{<<"username">> => <<"${clientid}">>}, selector => #{<<"username">> => <<"${clientid}">>},
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{
}, name => <<"sha256">>,
result => {error,not_authorized} salt_position => <<"prefix">>
}, }
},
result => {ok, #{is_superuser => true}}
},
#{data => #{ #{
username => <<"bcrypt1">>, data => #{
password_hash => username => <<"bcrypt">>,
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, password_hash =>
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
is_superuser => <<"0">> salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
}, is_superuser => 0
credentials => #{ },
username => <<"bcrypt1">>, credentials => #{
password => <<"bcrypt">> username => <<"bcrypt">>,
}, password => <<"bcrypt">>
config_params => #{ },
selector => #{<<"userid">> => <<"${clientid}">>}, config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {error,not_authorized} result => {ok, #{is_superuser => false}}
}, },
#{data => #{ #{
username => <<"bcrypt2">>, data => #{
password_hash => username => <<"bcrypt0">>,
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, password_hash =>
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
is_superuser => <<"0">> salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
}, is_superuser => <<"0">>
credentials => #{ },
username => <<"bcrypt2">>, credentials => #{
% Wrong password username => <<"bcrypt0">>,
password => <<"wrongpass">> password => <<"bcrypt">>
}, },
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>} % clientid variable & username credentials
}, selector => #{<<"username">> => <<"${clientid}">>},
result => {error,bad_username_or_password} password_hash_algorithm => #{name => <<"bcrypt">>}
} },
result => {error, not_authorized}
},
#{
data => #{
username => <<"bcrypt1">>,
password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt1">>,
password => <<"bcrypt">>
},
config_params => #{
selector => #{<<"userid">> => <<"${clientid}">>},
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, not_authorized}
},
#{
data => #{
username => <<"bcrypt2">>,
password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt2">>,
% Wrong password
password => <<"wrongpass">>
},
config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, bad_username_or_password}
}
]. ].
init_seeds() -> init_seeds() ->
@ -390,14 +428,14 @@ drop_seeds() ->
ok. ok.
mongo_server() -> mongo_server() ->
iolist_to_binary(io_lib:format("~s",[?MONGO_HOST])). iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
mongo_config() -> mongo_config() ->
[ [
{database, <<"mqtt">>}, {database, <<"mqtt">>},
{host, ?MONGO_HOST}, {host, ?MONGO_HOST},
{port, ?MONGO_DEFAULT_PORT}, {port, ?MONGO_DEFAULT_PORT},
{register, ?MONGO_CLIENT} {register, ?MONGO_CLIENT}
]. ].
start_apps(Apps) -> start_apps(Apps) ->

View File

@ -25,7 +25,6 @@
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(MONGO_HOST, "mongo-tls"). -define(MONGO_HOST, "mongo-tls").
-define(PATH, [authentication]). -define(PATH, [authentication]).
@ -37,8 +36,9 @@ init_per_testcase(_TestCase, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authentication:initialize_authentication(?GLOBAL, []), emqx_authentication:initialize_authentication(?GLOBAL, []),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
Config. Config.
init_per_suite(Config) -> init_per_suite(Config) ->
@ -54,8 +54,9 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
@ -72,69 +73,90 @@ end_per_suite(_Config) ->
t_create(_Config) -> t_create(_Config) ->
?check_trace( ?check_trace(
create_mongo_auth_with_ssl_opts( create_mongo_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}), <<"versions">> => [<<"tlsv1.2">>],
fun({ok, _}, Trace) -> <<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
}
),
fun({ok, _}, Trace) ->
?assertMatch( ?assertMatch(
[ok | _], [ok | _],
?projection( ?projection(
status, status,
?of_kind(emqx_connector_mongo_health_check, Trace))) ?of_kind(emqx_connector_mongo_health_check, Trace)
end). )
)
end
).
t_create_invalid_server_name(_Config) -> t_create_invalid_server_name(_Config) ->
?check_trace( ?check_trace(
create_mongo_auth_with_ssl_opts( create_mongo_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>, #{
<<"verify">> => <<"verify_peer">>}), <<"server_name_indication">> => <<"authn-server-unknown-host">>,
fun(_, Trace) -> <<"verify">> => <<"verify_peer">>
?assertNotEqual( }
[ok], ),
?projection( fun(_, Trace) ->
status, ?assertNotEqual(
?of_kind(emqx_connector_mongo_health_check, Trace))) [ok],
end). ?projection(
status,
?of_kind(emqx_connector_mongo_health_check, Trace)
)
)
end
).
%% docker-compose-mongo-single-tls.yaml: %% docker-compose-mongo-single-tls.yaml:
%% --tlsDisabledProtocols TLS1_0,TLS1_1 %% --tlsDisabledProtocols TLS1_0,TLS1_1
t_create_invalid_version(_Config) -> t_create_invalid_version(_Config) ->
?check_trace( ?check_trace(
create_mongo_auth_with_ssl_opts( create_mongo_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.1">>]}), <<"verify">> => <<"verify_peer">>,
fun(_, Trace) -> <<"versions">> => [<<"tlsv1.1">>]
?assertNotEqual( }
[ok], ),
?projection( fun(_, Trace) ->
status, ?assertNotEqual(
?of_kind(emqx_connector_mongo_health_check, Trace))) [ok],
end). ?projection(
status,
?of_kind(emqx_connector_mongo_health_check, Trace)
)
)
end
).
%% docker-compose-mongo-single-tls.yaml: %% docker-compose-mongo-single-tls.yaml:
%% --setParameter opensslCipherConfig='HIGH:!EXPORT:!aNULL:!DHE:!kDHE@STRENGTH' %% --setParameter opensslCipherConfig='HIGH:!EXPORT:!aNULL:!DHE:!kDHE@STRENGTH'
t_invalid_ciphers(_Config) -> t_invalid_ciphers(_Config) ->
?check_trace( ?check_trace(
create_mongo_auth_with_ssl_opts( create_mongo_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"DHE-RSA-AES256-GCM-SHA384">>]}), <<"versions">> => [<<"tlsv1.2">>],
fun(_, Trace) -> <<"ciphers">> => [<<"DHE-RSA-AES256-GCM-SHA384">>]
?assertNotEqual( }
[ok], ),
?projection( fun(_, Trace) ->
status, ?assertNotEqual(
?of_kind(emqx_connector_mongo_health_check, Trace))) [ok],
end). ?projection(
status,
?of_kind(emqx_connector_mongo_health_check, Trace)
)
)
end
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -148,35 +170,38 @@ create_mongo_auth_with_ssl_opts(SpecificSSLOpts) ->
raw_mongo_auth_config(SpecificSSLOpts) -> raw_mongo_auth_config(SpecificSSLOpts) ->
SSLOpts = maps:merge( SSLOpts = maps:merge(
emqx_authn_test_lib:client_ssl_cert_opts(), emqx_authn_test_lib:client_ssl_cert_opts(),
#{enable => <<"true">>}), #{enable => <<"true">>}
),
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
password_hash_algorithm => #{name => <<"plain">>, password_hash_algorithm => #{
salt_position => <<"suffix">>}, name => <<"plain">>,
enable => <<"true">>, salt_position => <<"suffix">>
},
enable => <<"true">>,
backend => <<"mongodb">>, backend => <<"mongodb">>,
pool_size => 2, pool_size => 2,
mongo_type => <<"single">>, mongo_type => <<"single">>,
database => <<"mqtt">>, database => <<"mqtt">>,
collection => <<"users">>, collection => <<"users">>,
server => mongo_server(), server => mongo_server(),
w_mode => <<"unsafe">>, w_mode => <<"unsafe">>,
selector => #{<<"username">> => <<"${username}">>}, selector => #{<<"username">> => <<"${username}">>},
password_hash_field => <<"password_hash">>, password_hash_field => <<"password_hash">>,
salt_field => <<"salt">>, salt_field => <<"salt">>,
is_superuser_field => <<"is_superuser">>, is_superuser_field => <<"is_superuser">>,
topology => #{ topology => #{
server_selection_timeout_ms => <<"10000ms">> server_selection_timeout_ms => <<"10000ms">>
}, },
ssl => maps:merge(SSLOpts, SpecificSSLOpts) ssl => maps:merge(SSLOpts, SpecificSSLOpts)
}. }.
mongo_server() -> mongo_server() ->
iolist_to_binary(io_lib:format("~s",[?MONGO_HOST])). iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -21,28 +21,36 @@
-include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl").
%% API %% API
-export([start_link/2, -export([
stop/1]). start_link/2,
stop/1
]).
-export([send/2]). -export([send/2]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, -export([
handle_call/3, init/1,
handle_cast/2, handle_call/3,
handle_info/2, handle_cast/2,
terminate/2]). handle_info/2,
terminate/2
]).
-define(TIMEOUT, 1000). -define(TIMEOUT, 1000).
-define(TCP_OPTIONS, [binary, {packet, raw}, {active, once}, -define(TCP_OPTIONS, [
{nodelay, true}]). binary,
{packet, raw},
{active, once},
{nodelay, true}
]).
-define(PARSE_OPTIONS, -define(PARSE_OPTIONS, #{
#{strict_mode => false, strict_mode => false,
max_size => ?MAX_PACKET_SIZE, max_size => ?MAX_PACKET_SIZE,
version => ?MQTT_PROTO_V5 version => ?MQTT_PROTO_V5
}). }).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% API %% API
@ -63,26 +71,30 @@ send(Pid, Packet) ->
init([Host, Port, Owner]) -> init([Host, Port, Owner]) ->
{ok, Socket} = gen_tcp:connect(Host, Port, ?TCP_OPTIONS, ?TIMEOUT), {ok, Socket} = gen_tcp:connect(Host, Port, ?TCP_OPTIONS, ?TIMEOUT),
{ok, #{owner => Owner, {ok, #{
socket => Socket, owner => Owner,
parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS) socket => Socket,
}}. parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS)
}}.
handle_info({tcp, _Sock, Data}, #{parse_state := PSt, handle_info(
owner := Owner, {tcp, _Sock, Data},
socket := Socket} = St) -> #{
parse_state := PSt,
owner := Owner,
socket := Socket
} = St
) ->
{NewPSt, Packets} = process_incoming(PSt, Data, []), {NewPSt, Packets} = process_incoming(PSt, Data, []),
ok = deliver(Owner, Packets), ok = deliver(Owner, Packets),
ok = run_sock(Socket), ok = run_sock(Socket),
{noreply, St#{parse_state => NewPSt}}; {noreply, St#{parse_state => NewPSt}};
handle_info({tcp_closed, _Sock}, St) -> handle_info({tcp_closed, _Sock}, St) ->
{stop, normal, St}. {stop, normal, St}.
handle_call({send, Packet}, _From, #{socket := Socket} = St) -> handle_call({send, Packet}, _From, #{socket := Socket} = St) ->
ok = gen_tcp:send(Socket, emqx_frame:serialize(Packet, ?MQTT_PROTO_V5)), ok = gen_tcp:send(Socket, emqx_frame:serialize(Packet, ?MQTT_PROTO_V5)),
{reply, ok, St}; {reply, ok, St};
handle_call(stop, _From, #{socket := Socket} = St) -> handle_call(stop, _From, #{socket := Socket} = St) ->
ok = gen_tcp:close(Socket), ok = gen_tcp:close(Socket),
{stop, normal, ok, St}. {stop, normal, ok, St}.
@ -100,16 +112,16 @@ terminate(_Reason, _St) ->
process_incoming(PSt, Data, Packets) -> process_incoming(PSt, Data, Packets) ->
case emqx_frame:parse(Data, PSt) of case emqx_frame:parse(Data, PSt) of
{more, NewPSt} -> {more, NewPSt} ->
{NewPSt, lists:reverse(Packets)}; {NewPSt, lists:reverse(Packets)};
{ok, Packet, Rest, NewPSt} -> {ok, Packet, Rest, NewPSt} ->
process_incoming(NewPSt, Rest, [Packet | Packets]) process_incoming(NewPSt, Rest, [Packet | Packets])
end. end.
deliver(_Owner, []) -> ok; deliver(_Owner, []) ->
ok;
deliver(Owner, [Packet | Packets]) -> deliver(Owner, [Packet | Packets]) ->
Owner ! {packet, Packet}, Owner ! {packet, Packet},
deliver(Owner, Packets). deliver(Owner, Packets).
run_sock(Socket) -> run_sock(Socket) ->
inet:setopts(Socket, [{active, once}]). inet:setopts(Socket, [{active, once}]).

View File

@ -40,8 +40,9 @@ init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authentication:initialize_authentication(?GLOBAL, []), emqx_authentication:initialize_authentication(?GLOBAL, []),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
Config. Config.
init_per_group(require_seeds, Config) -> init_per_group(require_seeds, Config) ->
@ -59,11 +60,12 @@ init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource, emqx_connector]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?MYSQL_RESOURCE, ?MYSQL_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
emqx_connector_mysql, emqx_connector_mysql,
mysql_config(), mysql_config(),
#{}), #{}
),
Config; Config;
false -> false ->
{skip, no_mysql} {skip, no_mysql}
@ -71,8 +73,9 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
ok = emqx_resource:remove_local(?MYSQL_RESOURCE), ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
@ -85,8 +88,9 @@ t_create(_Config) ->
AuthConfig = raw_mysql_auth_config(), AuthConfig = raw_mysql_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_mysql}]} = emqx_authentication:list_authenticators(?GLOBAL), {ok, [#{provider := emqx_authn_mysql}]} = emqx_authentication:list_authenticators(?GLOBAL),
emqx_authn_test_lib:delete_config(?ResourceID). emqx_authn_test_lib:delete_config(?ResourceID).
@ -96,108 +100,132 @@ t_create_invalid(_Config) ->
InvalidConfigs = InvalidConfigs =
[ [
maps:without([server], AuthConfig), maps:without([server], AuthConfig),
AuthConfig#{server => <<"unknownhost:3333">>}, AuthConfig#{server => <<"unknownhost:3333">>},
AuthConfig#{password => <<"wrongpass">>}, AuthConfig#{password => <<"wrongpass">>},
AuthConfig#{database => <<"wrongdatabase">>} AuthConfig#{database => <<"wrongdatabase">>}
], ],
lists:foreach( lists:foreach(
fun(Config) -> fun(Config) ->
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
emqx_authn_test_lib:delete_config(?ResourceID), ),
{ok, _} = emqx_authentication:list_authenticators(?GLOBAL) emqx_authn_test_lib:delete_config(?ResourceID),
end, {ok, _} = emqx_authentication:list_authenticators(?GLOBAL)
InvalidConfigs). end,
InvalidConfigs
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = lists:foreach( ok = lists:foreach(
fun(Sample) -> fun(Sample) ->
ct:pal("test_user_auth sample: ~p", [Sample]), ct:pal("test_user_auth sample: ~p", [Sample]),
test_user_auth(Sample) test_user_auth(Sample)
end, end,
user_seeds()). user_seeds()
).
test_user_auth(#{credentials := Credentials0, test_user_auth(#{
config_params := SpecificConfigParams, credentials := Credentials0,
result := Result}) -> config_params := SpecificConfigParams,
result := Result
}) ->
AuthConfig = maps:merge(raw_mysql_auth_config(), SpecificConfigParams), AuthConfig = maps:merge(raw_mysql_auth_config(), SpecificConfigParams),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
Credentials = Credentials0#{ Credentials = Credentials0#{
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt protocol => mqtt
}, },
?assertEqual(Result, emqx_access_control:authenticate(Credentials)), ?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL). ?GLOBAL
).
t_destroy(_Config) -> t_destroy(_Config) ->
AuthConfig = raw_mysql_auth_config(), AuthConfig = raw_mysql_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_mysql, state := State}]} {ok, [#{provider := emqx_authn_mysql, state := State}]} =
= emqx_authentication:list_authenticators(?GLOBAL), emqx_authentication:list_authenticators(?GLOBAL),
{ok, _} = emqx_authn_mysql:authenticate( {ok, _} = emqx_authn_mysql:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">> username => <<"plain">>,
}, password => <<"plain">>
State), },
State
),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
% Authenticator should not be usable anymore % Authenticator should not be usable anymore
?assertMatch( ?assertMatch(
ignore, ignore,
emqx_authn_mysql:authenticate( emqx_authn_mysql:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">> username => <<"plain">>,
}, password => <<"plain">>
State)). },
State
)
).
t_update(_Config) -> t_update(_Config) ->
CorrectConfig = raw_mysql_auth_config(), CorrectConfig = raw_mysql_auth_config(),
IncorrectConfig = IncorrectConfig =
CorrectConfig#{ CorrectConfig#{
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser query =>
FROM wrong_table where username = ${username} LIMIT 1">>}, <<
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
" FROM wrong_table where username = ${username} LIMIT 1"
>>
},
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, IncorrectConfig}), {create_authenticator, ?GLOBAL, IncorrectConfig}
),
{error, not_authorized} = emqx_access_control:authenticate( {error, not_authorized} = emqx_access_control:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}), protocol => mqtt
}
),
% We update with config with correct query, provider should update and work properly % We update with config with correct query, provider should update and work properly
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{update_authenticator, ?GLOBAL, <<"password_based:mysql">>, CorrectConfig}), {update_authenticator, ?GLOBAL, <<"password_based:mysql">>, CorrectConfig}
),
{ok,_} = emqx_access_control:authenticate( {ok, _} = emqx_access_control:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}). protocol => mqtt
}
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -205,207 +233,248 @@ t_update(_Config) ->
raw_mysql_auth_config() -> raw_mysql_auth_config() ->
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
password_hash_algorithm => #{name => <<"plain">>, password_hash_algorithm => #{
salt_position => <<"suffix">>}, name => <<"plain">>,
enable => <<"true">>, salt_position => <<"suffix">>
},
enable => <<"true">>,
backend => <<"mysql">>, backend => <<"mysql">>,
database => <<"mqtt">>, database => <<"mqtt">>,
username => <<"root">>, username => <<"root">>,
password => <<"public">>, password => <<"public">>,
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser query =>
FROM users where username = ${username} LIMIT 1">>, <<
server => mysql_server() "SELECT password_hash, salt, is_superuser_str as is_superuser\n"
}. " FROM users where username = ${username} LIMIT 1"
>>,
server => mysql_server()
}.
user_seeds() -> user_seeds() ->
[#{data => #{ [
username => "plain", #{
password_hash => "plainsalt", data => #{
salt => "salt", username => "plain",
is_superuser_str => "1" password_hash => "plainsalt",
}, salt => "salt",
credentials => #{ is_superuser_str => "1"
username => <<"plain">>, },
password => <<"plain">>}, credentials => #{
config_params => #{}, username => <<"plain">>,
result => {ok,#{is_superuser => true}} password => <<"plain">>
}, },
config_params => #{},
#{data => #{ result => {ok, #{is_superuser => true}}
username => "md5",
password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
salt => "salt",
is_superuser_str => "0"
},
credentials => #{
username => <<"md5">>,
password => <<"md5">>
},
config_params => #{
password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">>}
},
result => {ok,#{is_superuser => false}}
},
#{data => #{
username => "sha256",
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
salt => "salt",
is_superuser_int => 1
}, },
credentials => #{
clientid => <<"sha256">>,
password => <<"sha256">>
},
config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${clientid} LIMIT 1">>,
password_hash_algorithm => #{name => <<"sha256">>,
salt_position => <<"prefix">>}
},
result => {ok,#{is_superuser => true}}
},
#{data => #{ #{
username => <<"bcrypt">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => "md5",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
is_superuser_int => 0 salt => "salt",
}, is_superuser_str => "0"
credentials => #{ },
username => <<"bcrypt">>, credentials => #{
password => <<"bcrypt">> username => <<"md5">>,
}, password => <<"md5">>
config_params => #{ },
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser config_params => #{
FROM users where username = ${username} LIMIT 1">>, password_hash_algorithm => #{
password_hash_algorithm => #{name => <<"bcrypt">>} name => <<"md5">>,
}, salt_position => <<"suffix">>
result => {ok,#{is_superuser => false}} }
}, },
result => {ok, #{is_superuser => false}}
},
#{data => #{ #{
username => <<"bcrypt">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => "sha256",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve" password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
}, salt => "salt",
credentials => #{ is_superuser_int => 1
username => <<"bcrypt">>, },
password => <<"bcrypt">> credentials => #{
}, clientid => <<"sha256">>,
config_params => #{ password => <<"sha256">>
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser },
FROM users where username = ${username} LIMIT 1">>, config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>} query =>
}, <<
result => {ok,#{is_superuser => false}} "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
}, " FROM users where username = ${clientid} LIMIT 1"
>>,
password_hash_algorithm => #{
name => <<"sha256">>,
salt_position => <<"prefix">>
}
},
result => {ok, #{is_superuser => true}}
},
#{data => #{ #{
username => <<"bcrypt0">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => <<"bcrypt">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
is_superuser_str => "0" salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
}, is_superuser_int => 0
credentials => #{ },
username => <<"bcrypt0">>, credentials => #{
password => <<"bcrypt">> username => <<"bcrypt">>,
}, password => <<"bcrypt">>
config_params => #{ },
% clientid variable & username credentials config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser query =>
FROM users where username = ${clientid} LIMIT 1">>, <<
password_hash_algorithm => #{name => <<"bcrypt">>} "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
}, " FROM users where username = ${username} LIMIT 1"
result => {error,not_authorized} >>,
}, password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {ok, #{is_superuser => false}}
},
#{data => #{ #{
username => <<"bcrypt1">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => <<"bcrypt">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
is_superuser_str => "0" salt => "$2b$12$wtY3h20mUjjmeaClpqZVve"
}, },
credentials => #{ credentials => #{
username => <<"bcrypt1">>, username => <<"bcrypt">>,
password => <<"bcrypt">> password => <<"bcrypt">>
}, },
config_params => #{ config_params => #{
% Bad keys in query query =>
query => <<"SELECT 1 AS unknown_field <<
FROM users where username = ${username} LIMIT 1">>, "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
password_hash_algorithm => #{name => <<"bcrypt">>} " FROM users where username = ${username} LIMIT 1"
}, >>,
result => {error,not_authorized} password_hash_algorithm => #{name => <<"bcrypt">>}
}, },
result => {ok, #{is_superuser => false}}
},
#{data => #{ #{
username => <<"bcrypt2">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => <<"bcrypt0">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
is_superuser => "0" salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
}, is_superuser_str => "0"
credentials => #{ },
username => <<"bcrypt2">>, credentials => #{
% Wrong password username => <<"bcrypt0">>,
password => <<"wrongpass">> password => <<"bcrypt">>
}, },
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>} % clientid variable & username credentials
}, query =>
result => {error,bad_username_or_password} <<
} "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
" FROM users where username = ${clientid} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, not_authorized}
},
#{
data => #{
username => <<"bcrypt1">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
is_superuser_str => "0"
},
credentials => #{
username => <<"bcrypt1">>,
password => <<"bcrypt">>
},
config_params => #{
% Bad keys in query
query =>
<<
"SELECT 1 AS unknown_field\n"
" FROM users where username = ${username} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, not_authorized}
},
#{
data => #{
username => <<"bcrypt2">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
is_superuser => "0"
},
credentials => #{
username => <<"bcrypt2">>,
% Wrong password
password => <<"wrongpass">>
},
config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, bad_username_or_password}
}
]. ].
init_seeds() -> init_seeds() ->
ok = drop_seeds(), ok = drop_seeds(),
ok = q("CREATE TABLE users( ok = q(
username VARCHAR(255), "CREATE TABLE users(\n"
password_hash VARCHAR(255), " username VARCHAR(255),\n"
salt VARCHAR(255), " password_hash VARCHAR(255),\n"
is_superuser_str VARCHAR(255), " salt VARCHAR(255),\n"
is_superuser_int TINYINT)"), " is_superuser_str VARCHAR(255),\n"
" is_superuser_int TINYINT)"
),
Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int], Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int],
InsertQuery = "INSERT INTO users(username, password_hash, salt, " InsertQuery =
" is_superuser_str, is_superuser_int) VALUES(?, ?, ?, ?, ?)", "INSERT INTO users(username, password_hash, salt, "
" is_superuser_str, is_superuser_int) VALUES(?, ?, ?, ?, ?)",
lists:foreach( lists:foreach(
fun(#{data := Values}) -> fun(#{data := Values}) ->
Params = [maps:get(F, Values, null) || F <- Fields], Params = [maps:get(F, Values, null) || F <- Fields],
ok = q(InsertQuery, Params) ok = q(InsertQuery, Params)
end, end,
user_seeds()). user_seeds()
).
q(Sql) -> q(Sql) ->
emqx_resource:query( emqx_resource:query(
?MYSQL_RESOURCE, ?MYSQL_RESOURCE,
{sql, Sql}). {sql, Sql}
).
q(Sql, Params) -> q(Sql, Params) ->
emqx_resource:query( emqx_resource:query(
?MYSQL_RESOURCE, ?MYSQL_RESOURCE,
{sql, Sql, Params}). {sql, Sql, Params}
).
drop_seeds() -> drop_seeds() ->
ok = q("DROP TABLE IF EXISTS users"). ok = q("DROP TABLE IF EXISTS users").
mysql_server() -> mysql_server() ->
iolist_to_binary(io_lib:format("~s",[?MYSQL_HOST])). iolist_to_binary(io_lib:format("~s", [?MYSQL_HOST])).
mysql_config() -> mysql_config() ->
#{auto_reconnect => true, #{
database => <<"mqtt">>, auto_reconnect => true,
username => <<"root">>, database => <<"mqtt">>,
password => <<"public">>, username => <<"root">>,
pool_size => 8, password => <<"public">>,
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT}, pool_size => 8,
ssl => #{enable => false} server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
}. ssl => #{enable => false}
}.
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authentication:initialize_authentication(?GLOBAL, []), emqx_authentication:initialize_authentication(?GLOBAL, []),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
Config. Config.
init_per_suite(Config) -> init_per_suite(Config) ->
@ -56,8 +57,9 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
@ -70,38 +72,53 @@ t_create(_Config) ->
%% -connect authn-server:3306 -starttls mysql \ %% -connect authn-server:3306 -starttls mysql \
%% -cert client.crt -key client.key -CAfile ca.crt %% -cert client.crt -key client.key -CAfile ca.crt
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_mysql_auth_with_ssl_opts( create_mysql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]})). <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
}
)
).
t_create_invalid(_Config) -> t_create_invalid(_Config) ->
%% invalid server_name %% invalid server_name
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_mysql_auth_with_ssl_opts( create_mysql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>, #{
<<"verify">> => <<"verify_peer">>})), <<"server_name_indication">> => <<"authn-server-unknown-host">>,
<<"verify">> => <<"verify_peer">>
}
)
),
emqx_authn_test_lib:delete_config(?ResourceID), emqx_authn_test_lib:delete_config(?ResourceID),
%% incompatible versions %% incompatible versions
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_mysql_auth_with_ssl_opts( create_mysql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.1">>]})), <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>]
}
)
),
emqx_authn_test_lib:delete_config(?ResourceID), emqx_authn_test_lib:delete_config(?ResourceID),
%% incompatible ciphers %% incompatible ciphers
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_mysql_auth_with_ssl_opts( create_mysql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]})). <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]
}
)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -113,27 +130,33 @@ create_mysql_auth_with_ssl_opts(SpecificSSLOpts) ->
raw_mysql_auth_config(SpecificSSLOpts) -> raw_mysql_auth_config(SpecificSSLOpts) ->
SSLOpts = maps:merge( SSLOpts = maps:merge(
emqx_authn_test_lib:client_ssl_cert_opts(), emqx_authn_test_lib:client_ssl_cert_opts(),
#{enable => <<"true">>}), #{enable => <<"true">>}
),
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
password_hash_algorithm => #{name => <<"plain">>, password_hash_algorithm => #{
salt_position => <<"suffix">>}, name => <<"plain">>,
enable => <<"true">>, salt_position => <<"suffix">>
},
enable => <<"true">>,
backend => <<"mysql">>, backend => <<"mysql">>,
database => <<"mqtt">>, database => <<"mqtt">>,
username => <<"root">>, username => <<"root">>,
password => <<"public">>, password => <<"public">>,
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser query =>
FROM users where username = ${username} LIMIT 1">>, <<
server => mysql_server(), "SELECT password_hash, salt, is_superuser_str as is_superuser\n"
ssl => maps:merge(SSLOpts, SpecificSSLOpts) " FROM users where username = ${username} LIMIT 1"
}. >>,
server => mysql_server(),
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
}.
mysql_server() -> mysql_server() ->
iolist_to_binary(io_lib:format("~s",[?MYSQL_HOST])). iolist_to_binary(io_lib:format("~s", [?MYSQL_HOST])).
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -38,118 +38,150 @@ end_per_suite(_Config) ->
ok. ok.
t_gen_salt(_Config) -> t_gen_salt(_Config) ->
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES] Algorithms =
++ [#{name => bcrypt, salt_rounds => 10}], [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES] ++
[#{name => bcrypt, salt_rounds => 10}],
lists:foreach( lists:foreach(
fun(Algorithm) -> fun(Algorithm) ->
Salt = emqx_authn_password_hashing:gen_salt(Algorithm), Salt = emqx_authn_password_hashing:gen_salt(Algorithm),
ct:pal("gen_salt(~p): ~p", [Algorithm, Salt]), ct:pal("gen_salt(~p): ~p", [Algorithm, Salt]),
?assert(is_binary(Salt)) ?assert(is_binary(Salt))
end, end,
Algorithms). Algorithms
).
t_init(_Config) -> t_init(_Config) ->
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES] Algorithms =
++ [#{name => bcrypt, salt_rounds => 10}], [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES] ++
[#{name => bcrypt, salt_rounds => 10}],
lists:foreach( lists:foreach(
fun(Algorithm) -> fun(Algorithm) ->
ok = emqx_authn_password_hashing:init(Algorithm) ok = emqx_authn_password_hashing:init(Algorithm)
end, end,
Algorithms). Algorithms
).
t_check_password(_Config) -> t_check_password(_Config) ->
lists:foreach( lists:foreach(
fun test_check_password/1, fun test_check_password/1,
hash_examples()). hash_examples()
).
test_check_password(#{ test_check_password(
password_hash := Hash, #{
salt := Salt, password_hash := Hash,
password := Password, salt := Salt,
password_hash_algorithm := Algorithm password := Password,
} = Sample) -> password_hash_algorithm := Algorithm
} = Sample
) ->
ct:pal("t_check_password sample: ~p", [Sample]), ct:pal("t_check_password sample: ~p", [Sample]),
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password), true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password),
false = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, <<"wrongpass">>). false = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, <<"wrongpass">>).
t_hash(_Config) -> t_hash(_Config) ->
lists:foreach( lists:foreach(
fun test_hash/1, fun test_hash/1,
hash_examples()). hash_examples()
).
test_hash(#{password := Password, test_hash(
password_hash_algorithm := Algorithm #{
} = Sample) -> password := Password,
password_hash_algorithm := Algorithm
} = Sample
) ->
ct:pal("t_hash sample: ~p", [Sample]), ct:pal("t_hash sample: ~p", [Sample]),
{Hash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password), {Hash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password),
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password). true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password).
hash_examples() -> hash_examples() ->
[#{ [
password_hash => <<"plainsalt">>, #{
salt => <<"salt">>, password_hash => <<"plainsalt">>,
password => <<"plain">>, salt => <<"salt">>,
password_hash_algorithm => #{name => plain, password => <<"plain">>,
salt_position => suffix} password_hash_algorithm => #{
}, name => plain,
#{ salt_position => suffix
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>, }
salt => <<"salt">>, },
password => <<"md5">>, #{
password_hash_algorithm => #{name => md5, password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt_position => suffix} salt => <<"salt">>,
}, password => <<"md5">>,
#{ password_hash_algorithm => #{
password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>, name => md5,
salt => <<"salt">>, salt_position => suffix
password => <<"sha">>, }
password_hash_algorithm => #{name => sha, },
salt_position => prefix} #{
}, password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>,
#{ salt => <<"salt">>,
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>, password => <<"sha">>,
salt => <<"salt">>, password_hash_algorithm => #{
password => <<"sha256">>, name => sha,
password_hash_algorithm => #{name => sha256, salt_position => prefix
salt_position => prefix} }
}, },
#{ #{
password_hash => <<"a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8" password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd">>, salt => <<"salt">>,
salt => <<"salt">>, password => <<"sha256">>,
password => <<"sha512">>, password_hash_algorithm => #{
password_hash_algorithm => #{name => sha512, name => sha256,
salt_position => prefix} salt_position => prefix
}, }
#{ },
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, #{
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, password_hash => <<
password => <<"bcrypt">>, "a1509ab67bfacbad020927b5ac9d91e9100a82e33a0ebb01459367ce921c0aa8"
"157aa5652f94bc84fa3babc08283e44887d61c48bcf8ad7bcb3259ee7d0eafcd"
>>,
salt => <<"salt">>,
password => <<"sha512">>,
password_hash_algorithm => #{
name => sha512,
salt_position => prefix
}
},
#{
password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
password => <<"bcrypt">>,
password_hash_algorithm => #{name => bcrypt, password_hash_algorithm => #{
salt_rounds => 10} name => bcrypt,
}, salt_rounds => 10
}
},
#{ #{
password_hash => <<"01dbee7f4a9e243e988b62c73cda935d" password_hash => <<
"a05378b93244ec8f48a99e61ad799d86">>, "01dbee7f4a9e243e988b62c73cda935d"
salt => <<"ATHENA.MIT.EDUraeburn">>, "a05378b93244ec8f48a99e61ad799d86"
password => <<"password">>, >>,
salt => <<"ATHENA.MIT.EDUraeburn">>,
password => <<"password">>,
password_hash_algorithm => #{name => pbkdf2, password_hash_algorithm => #{
iterations => 2, name => pbkdf2,
dk_length => 32, iterations => 2,
mac_fun => sha} dk_length => 32,
}, mac_fun => sha
#{ }
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>, },
salt => <<"ATHENA.MIT.EDUraeburn">>, #{
password => <<"password">>, password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
salt => <<"ATHENA.MIT.EDUraeburn">>,
password => <<"password">>,
password_hash_algorithm => #{name => pbkdf2, password_hash_algorithm => #{
iterations => 2, name => pbkdf2,
mac_fun => sha} iterations => 2,
} mac_fun => sha
}
}
]. ].

View File

@ -41,8 +41,9 @@ init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authentication:initialize_authentication(?GLOBAL, []), emqx_authentication:initialize_authentication(?GLOBAL, []),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
Config. Config.
init_per_group(require_seeds, Config) -> init_per_group(require_seeds, Config) ->
@ -60,11 +61,12 @@ init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource, emqx_connector]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?PGSQL_RESOURCE, ?PGSQL_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
emqx_connector_pgsql, emqx_connector_pgsql,
pgsql_config(), pgsql_config(),
#{}), #{}
),
Config; Config;
false -> false ->
{skip, no_pgsql} {skip, no_pgsql}
@ -72,8 +74,9 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
ok = emqx_resource:remove_local(?PGSQL_RESOURCE), ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
@ -86,8 +89,9 @@ t_create(_Config) ->
AuthConfig = raw_pgsql_auth_config(), AuthConfig = raw_pgsql_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_pgsql}]} = emqx_authentication:list_authenticators(?GLOBAL), {ok, [#{provider := emqx_authn_pgsql}]} = emqx_authentication:list_authenticators(?GLOBAL),
emqx_authn_test_lib:delete_config(?ResourceID). emqx_authn_test_lib:delete_config(?ResourceID).
@ -97,131 +101,156 @@ t_create_invalid(_Config) ->
InvalidConfigs = InvalidConfigs =
[ [
maps:without([server], AuthConfig), maps:without([server], AuthConfig),
AuthConfig#{server => <<"unknownhost:3333">>}, AuthConfig#{server => <<"unknownhost:3333">>},
AuthConfig#{password => <<"wrongpass">>}, AuthConfig#{password => <<"wrongpass">>},
AuthConfig#{database => <<"wrongdatabase">>} AuthConfig#{database => <<"wrongdatabase">>}
], ],
lists:foreach( lists:foreach(
fun(Config) -> fun(Config) ->
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
emqx_authn_test_lib:delete_config(?ResourceID), ),
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL) emqx_authn_test_lib:delete_config(?ResourceID),
end, {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
InvalidConfigs). end,
InvalidConfigs
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = lists:foreach( ok = lists:foreach(
fun(Sample) -> fun(Sample) ->
ct:pal("test_user_auth sample: ~p", [Sample]), ct:pal("test_user_auth sample: ~p", [Sample]),
test_user_auth(Sample) test_user_auth(Sample)
end, end,
user_seeds()). user_seeds()
).
test_user_auth(#{credentials := Credentials0, test_user_auth(#{
config_params := SpecificConfigParams, credentials := Credentials0,
result := Result}) -> config_params := SpecificConfigParams,
result := Result
}) ->
AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfigParams), AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfigParams),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
Credentials = Credentials0#{ Credentials = Credentials0#{
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt protocol => mqtt
}, },
?assertEqual(Result, emqx_access_control:authenticate(Credentials)), ?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL). ?GLOBAL
).
t_destroy(_Config) -> t_destroy(_Config) ->
AuthConfig = raw_pgsql_auth_config(), AuthConfig = raw_pgsql_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_pgsql, state := State}]} {ok, [#{provider := emqx_authn_pgsql, state := State}]} =
= emqx_authentication:list_authenticators(?GLOBAL), emqx_authentication:list_authenticators(?GLOBAL),
{ok, _} = emqx_authn_pgsql:authenticate( {ok, _} = emqx_authn_pgsql:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">> username => <<"plain">>,
}, password => <<"plain">>
State), },
State
),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
% Authenticator should not be usable anymore % Authenticator should not be usable anymore
?assertMatch( ?assertMatch(
ignore, ignore,
emqx_authn_pgsql:authenticate( emqx_authn_pgsql:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">> username => <<"plain">>,
}, password => <<"plain">>
State)). },
State
)
).
t_update(_Config) -> t_update(_Config) ->
CorrectConfig = raw_pgsql_auth_config(), CorrectConfig = raw_pgsql_auth_config(),
IncorrectConfig = IncorrectConfig =
CorrectConfig#{ CorrectConfig#{
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser query =>
FROM users where username = ${username} LIMIT 0">>}, <<
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
" FROM users where username = ${username} LIMIT 0"
>>
},
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, IncorrectConfig}), {create_authenticator, ?GLOBAL, IncorrectConfig}
),
{error, not_authorized} = emqx_access_control:authenticate( {error, not_authorized} = emqx_access_control:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}), protocol => mqtt
}
),
% We update with config with correct query, provider should update and work properly % We update with config with correct query, provider should update and work properly
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, CorrectConfig}), {update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, CorrectConfig}
),
{ok,_} = emqx_access_control:authenticate( {ok, _} = emqx_access_control:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}). protocol => mqtt
}
).
t_is_superuser(_Config) -> t_is_superuser(_Config) ->
Config = raw_pgsql_auth_config(), Config = raw_pgsql_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
),
Checks = [ Checks = [
{is_superuser_str, "0", false}, {is_superuser_str, "0", false},
{is_superuser_str, "", false}, {is_superuser_str, "", false},
{is_superuser_str, null, false}, {is_superuser_str, null, false},
{is_superuser_str, "1", true}, {is_superuser_str, "1", true},
{is_superuser_str, "val", true}, {is_superuser_str, "val", true},
{is_superuser_int, 0, false}, {is_superuser_int, 0, false},
{is_superuser_int, null, false}, {is_superuser_int, null, false},
{is_superuser_int, 1, true}, {is_superuser_int, 1, true},
{is_superuser_int, 123, true}, {is_superuser_int, 123, true},
{is_superuser_bool, false, false}, {is_superuser_bool, false, false},
{is_superuser_bool, null, false}, {is_superuser_bool, null, false},
{is_superuser_bool, true, true} {is_superuser_bool, true, true}
], ],
lists:foreach(fun test_is_superuser/1, Checks). lists:foreach(fun test_is_superuser/1, Checks).
@ -229,32 +258,36 @@ test_is_superuser({Field, Value, ExpectedValue}) ->
{ok, _} = q("DELETE FROM users"), {ok, _} = q("DELETE FROM users"),
UserData = #{ UserData = #{
username => "user", username => "user",
password_hash => "plainsalt", password_hash => "plainsalt",
salt => "salt", salt => "salt",
Field => Value Field => Value
}, },
ok = create_user(UserData), ok = create_user(UserData),
Query = "SELECT password_hash, salt, " ++ atom_to_list(Field) ++ " as is_superuser " Query =
"FROM users where username = ${username} LIMIT 1", "SELECT password_hash, salt, " ++ atom_to_list(Field) ++
" as is_superuser "
"FROM users where username = ${username} LIMIT 1",
Config = maps:put(query, Query, raw_pgsql_auth_config()), Config = maps:put(query, Query, raw_pgsql_auth_config()),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, Config}), {update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, Config}
),
Credentials = #{ Credentials = #{
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt, protocol => mqtt,
username => <<"user">>, username => <<"user">>,
password => <<"plain">> password => <<"plain">>
}, },
?assertEqual( ?assertEqual(
{ok, #{is_superuser => ExpectedValue}}, {ok, #{is_superuser => ExpectedValue}},
emqx_access_control:authenticate(Credentials)). emqx_access_control:authenticate(Credentials)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -262,167 +295,201 @@ test_is_superuser({Field, Value, ExpectedValue}) ->
raw_pgsql_auth_config() -> raw_pgsql_auth_config() ->
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
password_hash_algorithm => #{name => <<"plain">>, password_hash_algorithm => #{
salt_position => <<"suffix">>}, name => <<"plain">>,
enable => <<"true">>, salt_position => <<"suffix">>
},
enable => <<"true">>,
backend => <<"postgresql">>, backend => <<"postgresql">>,
database => <<"mqtt">>, database => <<"mqtt">>,
username => <<"root">>, username => <<"root">>,
password => <<"public">>, password => <<"public">>,
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser query =>
FROM users where username = ${username} LIMIT 1">>, <<
server => pgsql_server() "SELECT password_hash, salt, is_superuser_str as is_superuser\n"
}. " FROM users where username = ${username} LIMIT 1"
>>,
server => pgsql_server()
}.
user_seeds() -> user_seeds() ->
[#{data => #{ [
username => "plain", #{
password_hash => "plainsalt", data => #{
salt => "salt", username => "plain",
is_superuser_str => "1" password_hash => "plainsalt",
}, salt => "salt",
credentials => #{ is_superuser_str => "1"
username => <<"plain">>, },
password => <<"plain">>}, credentials => #{
config_params => #{}, username => <<"plain">>,
result => {ok,#{is_superuser => true}} password => <<"plain">>
}, },
config_params => #{},
#{data => #{ result => {ok, #{is_superuser => true}}
username => "md5",
password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
salt => "salt",
is_superuser_str => "0"
},
credentials => #{
username => <<"md5">>,
password => <<"md5">>
},
config_params => #{
password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">>}
},
result => {ok,#{is_superuser => false}}
},
#{data => #{
username => "sha256",
password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
salt => "salt",
is_superuser_int => 1
}, },
credentials => #{
clientid => <<"sha256">>,
password => <<"sha256">>
},
config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
FROM users where username = ${clientid} LIMIT 1">>,
password_hash_algorithm => #{name => <<"sha256">>,
salt_position => <<"prefix">>}
},
result => {ok,#{is_superuser => true}}
},
#{data => #{ #{
username => <<"bcrypt">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => "md5",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
is_superuser_int => 0 salt => "salt",
}, is_superuser_str => "0"
credentials => #{ },
username => <<"bcrypt">>, credentials => #{
password => <<"bcrypt">> username => <<"md5">>,
}, password => <<"md5">>
config_params => #{ },
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser config_params => #{
FROM users where username = ${username} LIMIT 1">>, password_hash_algorithm => #{
password_hash_algorithm => #{name => <<"bcrypt">>} name => <<"md5">>,
}, salt_position => <<"suffix">>
result => {ok,#{is_superuser => false}} }
}, },
result => {ok, #{is_superuser => false}}
},
#{data => #{ #{
username => <<"bcrypt0">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => "sha256",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
is_superuser_str => "0" salt => "salt",
}, is_superuser_int => 1
credentials => #{ },
username => <<"bcrypt0">>, credentials => #{
password => <<"bcrypt">> clientid => <<"sha256">>,
}, password => <<"sha256">>
config_params => #{ },
% clientid variable & username credentials config_params => #{
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser query =>
FROM users where username = ${clientid} LIMIT 1">>, <<
password_hash_algorithm => #{name => <<"bcrypt">>} "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
}, " FROM users where username = ${clientid} LIMIT 1"
result => {error,not_authorized} >>,
}, password_hash_algorithm => #{
name => <<"sha256">>,
salt_position => <<"prefix">>
}
},
result => {ok, #{is_superuser => true}}
},
#{data => #{ #{
username => <<"bcrypt1">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => <<"bcrypt">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
is_superuser_str => "0" salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
}, is_superuser_int => 0
credentials => #{ },
username => <<"bcrypt1">>, credentials => #{
password => <<"bcrypt">> username => <<"bcrypt">>,
}, password => <<"bcrypt">>
config_params => #{ },
% Bad keys in query config_params => #{
query => <<"SELECT 1 AS unknown_field query =>
FROM users where username = ${username} LIMIT 1">>, <<
password_hash_algorithm => #{name => <<"bcrypt">>} "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
}, " FROM users where username = ${username} LIMIT 1"
result => {error,not_authorized} >>,
}, password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {ok, #{is_superuser => false}}
},
#{data => #{ #{
username => <<"bcrypt2">>, data => #{
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u", username => <<"bcrypt0">>,
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve", password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
is_superuser => "0" salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
}, is_superuser_str => "0"
credentials => #{ },
username => <<"bcrypt2">>, credentials => #{
% Wrong password username => <<"bcrypt0">>,
password => <<"wrongpass">> password => <<"bcrypt">>
}, },
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>} % clientid variable & username credentials
}, query =>
result => {error,bad_username_or_password} <<
} "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
" FROM users where username = ${clientid} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, not_authorized}
},
#{
data => #{
username => <<"bcrypt1">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
is_superuser_str => "0"
},
credentials => #{
username => <<"bcrypt1">>,
password => <<"bcrypt">>
},
config_params => #{
% Bad keys in query
query =>
<<
"SELECT 1 AS unknown_field\n"
" FROM users where username = ${username} LIMIT 1"
>>,
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, not_authorized}
},
#{
data => #{
username => <<"bcrypt2">>,
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
is_superuser => "0"
},
credentials => #{
username => <<"bcrypt2">>,
% Wrong password
password => <<"wrongpass">>
},
config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, bad_username_or_password}
}
]. ].
init_seeds() -> init_seeds() ->
ok = drop_seeds(), ok = drop_seeds(),
{ok, _, _} = q("CREATE TABLE users( {ok, _, _} = q(
username varchar(255), "CREATE TABLE users(\n"
password_hash varchar(255), " username varchar(255),\n"
salt varchar(255), " password_hash varchar(255),\n"
is_superuser_str varchar(255), " salt varchar(255),\n"
is_superuser_int smallint, " is_superuser_str varchar(255),\n"
is_superuser_bool boolean)"), " is_superuser_int smallint,\n"
" is_superuser_bool boolean)"
),
lists:foreach( lists:foreach(
fun(#{data := Values}) -> fun(#{data := Values}) ->
ok = create_user(Values) ok = create_user(Values)
end, end,
user_seeds()). user_seeds()
).
create_user(Values) -> create_user(Values) ->
Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int, is_superuser_bool], Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int, is_superuser_bool],
InsertQuery = "INSERT INTO users(username, password_hash, salt," InsertQuery =
"is_superuser_str, is_superuser_int, is_superuser_bool) " "INSERT INTO users(username, password_hash, salt,"
"VALUES($1, $2, $3, $4, $5, $6)", "is_superuser_str, is_superuser_int, is_superuser_bool) "
"VALUES($1, $2, $3, $4, $5, $6)",
Params = [maps:get(F, Values, null) || F <- Fields], Params = [maps:get(F, Values, null) || F <- Fields],
{ok, 1} = q(InsertQuery, Params), {ok, 1} = q(InsertQuery, Params),
@ -430,30 +497,33 @@ create_user(Values) ->
q(Sql) -> q(Sql) ->
emqx_resource:query( emqx_resource:query(
?PGSQL_RESOURCE, ?PGSQL_RESOURCE,
{query, Sql}). {query, Sql}
).
q(Sql, Params) -> q(Sql, Params) ->
emqx_resource:query( emqx_resource:query(
?PGSQL_RESOURCE, ?PGSQL_RESOURCE,
{query, Sql, Params}). {query, Sql, Params}
).
drop_seeds() -> drop_seeds() ->
{ok, _, _} = q("DROP TABLE IF EXISTS users"), {ok, _, _} = q("DROP TABLE IF EXISTS users"),
ok. ok.
pgsql_server() -> pgsql_server() ->
iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])). iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
pgsql_config() -> pgsql_config() ->
#{auto_reconnect => true, #{
database => <<"mqtt">>, auto_reconnect => true,
username => <<"root">>, database => <<"mqtt">>,
password => <<"public">>, username => <<"root">>,
pool_size => 8, password => <<"public">>,
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT}, pool_size => 8,
ssl => #{enable => false} server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
}. ssl => #{enable => false}
}.
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authentication:initialize_authentication(?GLOBAL, []), emqx_authentication:initialize_authentication(?GLOBAL, []),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
Config. Config.
init_per_suite(Config) -> init_per_suite(Config) ->
@ -56,8 +57,9 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
@ -70,38 +72,53 @@ t_create(_Config) ->
%% -starttls postgres -connect authn-server:5432 \ %% -starttls postgres -connect authn-server:5432 \
%% -cert client.crt -key client.key -CAfile ca.crt %% -cert client.crt -key client.key -CAfile ca.crt
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_pgsql_auth_with_ssl_opts( create_pgsql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]})). <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
}
)
).
t_create_invalid(_Config) -> t_create_invalid(_Config) ->
%% invalid server_name %% invalid server_name
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_pgsql_auth_with_ssl_opts( create_pgsql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>, #{
<<"verify">> => <<"verify_peer">>})), <<"server_name_indication">> => <<"authn-server-unknown-host">>,
<<"verify">> => <<"verify_peer">>
}
)
),
emqx_authn_test_lib:delete_config(?ResourceID), emqx_authn_test_lib:delete_config(?ResourceID),
%% incompatible versions %% incompatible versions
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_pgsql_auth_with_ssl_opts( create_pgsql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.1">>]})), <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>]
}
)
),
emqx_authn_test_lib:delete_config(?ResourceID), emqx_authn_test_lib:delete_config(?ResourceID),
%% incompatible ciphers %% incompatible ciphers
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_pgsql_auth_with_ssl_opts( create_pgsql_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.2">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]})). <<"versions">> => [<<"tlsv1.2">>],
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]
}
)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -113,26 +130,29 @@ create_pgsql_auth_with_ssl_opts(SpecificSSLOpts) ->
raw_pgsql_auth_config(SpecificSSLOpts) -> raw_pgsql_auth_config(SpecificSSLOpts) ->
SSLOpts = maps:merge( SSLOpts = maps:merge(
emqx_authn_test_lib:client_ssl_cert_opts(), emqx_authn_test_lib:client_ssl_cert_opts(),
#{enable => <<"true">>}), #{enable => <<"true">>}
),
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
password_hash_algorithm => #{name => <<"plain">>, password_hash_algorithm => #{
salt_position => <<"suffix">>}, name => <<"plain">>,
enable => <<"true">>, salt_position => <<"suffix">>
},
enable => <<"true">>,
backend => <<"postgresql">>, backend => <<"postgresql">>,
database => <<"mqtt">>, database => <<"mqtt">>,
username => <<"root">>, username => <<"root">>,
password => <<"public">>, password => <<"public">>,
query => <<"SELECT 1">>, query => <<"SELECT 1">>,
server => pgsql_server(), server => pgsql_server(),
ssl => maps:merge(SSLOpts, SpecificSSLOpts) ssl => maps:merge(SSLOpts, SpecificSSLOpts)
}. }.
pgsql_server() -> pgsql_server() ->
iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])). iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -40,8 +40,9 @@ init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authentication:initialize_authentication(?GLOBAL, []), emqx_authentication:initialize_authentication(?GLOBAL, []),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
Config. Config.
init_per_group(require_seeds, Config) -> init_per_group(require_seeds, Config) ->
@ -59,11 +60,12 @@ init_per_suite(Config) ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource, emqx_connector]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?REDIS_RESOURCE, ?REDIS_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
emqx_connector_redis, emqx_connector_redis,
redis_config(), redis_config(),
#{}), #{}
),
Config; Config;
false -> false ->
{skip, no_redis} {skip, no_redis}
@ -71,8 +73,9 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
ok = emqx_resource:remove_local(?REDIS_RESOURCE), ok = emqx_resource:remove_local(?REDIS_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
@ -86,8 +89,9 @@ t_create(_Config) ->
AuthConfig = raw_redis_auth_config(), AuthConfig = raw_redis_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_redis}]} = emqx_authentication:list_authenticators(?GLOBAL). {ok, [#{provider := emqx_authn_redis}]} = emqx_authentication:list_authenticators(?GLOBAL).
@ -95,126 +99,152 @@ t_create_invalid(_Config) ->
AuthConfig = raw_redis_auth_config(), AuthConfig = raw_redis_auth_config(),
InvalidConfigs = InvalidConfigs =
[ [
AuthConfig#{ AuthConfig#{
cmd => <<"MGET password_hash:${username} salt:${username}">>}, cmd => <<"MGET password_hash:${username} salt:${username}">>
AuthConfig#{ },
cmd => <<"HMGET mqtt_user:${username} password_hash invalid_field">>}, AuthConfig#{
AuthConfig#{ cmd => <<"HMGET mqtt_user:${username} password_hash invalid_field">>
cmd => <<"HMGET mqtt_user:${username} salt is_superuser">>} },
AuthConfig#{
cmd => <<"HMGET mqtt_user:${username} salt is_superuser">>
}
], ],
lists:foreach( lists:foreach(
fun(Config) -> fun(Config) ->
{error, _} = emqx:update_config( {error, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
),
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL) {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
end, end,
InvalidConfigs), InvalidConfigs
),
InvalidConfigs1 = InvalidConfigs1 =
[ [
maps:without([server], AuthConfig), maps:without([server], AuthConfig),
AuthConfig#{server => <<"unknownhost:3333">>}, AuthConfig#{server => <<"unknownhost:3333">>},
AuthConfig#{password => <<"wrongpass">>}, AuthConfig#{password => <<"wrongpass">>},
AuthConfig#{database => <<"5678">>} AuthConfig#{database => <<"5678">>}
], ],
lists:foreach( lists:foreach(
fun(Config) -> fun(Config) ->
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
emqx_authn_test_lib:delete_config(?ResourceID), ),
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL) emqx_authn_test_lib:delete_config(?ResourceID),
end, {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
InvalidConfigs1). end,
InvalidConfigs1
).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = lists:foreach( ok = lists:foreach(
fun(Sample) -> fun(Sample) ->
ct:pal("test_user_auth sample: ~p", [Sample]), ct:pal("test_user_auth sample: ~p", [Sample]),
test_user_auth(Sample) test_user_auth(Sample)
end, end,
user_seeds()). user_seeds()
).
test_user_auth(#{credentials := Credentials0, test_user_auth(#{
config_params := SpecificConfigParams, credentials := Credentials0,
result := Result}) -> config_params := SpecificConfigParams,
result := Result
}) ->
AuthConfig = maps:merge(raw_redis_auth_config(), SpecificConfigParams), AuthConfig = maps:merge(raw_redis_auth_config(), SpecificConfigParams),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
Credentials = Credentials0#{ Credentials = Credentials0#{
listener => 'tcp:default', listener => 'tcp:default',
protocol => mqtt protocol => mqtt
}, },
?assertEqual(Result, emqx_access_control:authenticate(Credentials)), ?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL). ?GLOBAL
).
t_destroy(_Config) -> t_destroy(_Config) ->
AuthConfig = raw_redis_auth_config(), AuthConfig = raw_redis_auth_config(),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}), {create_authenticator, ?GLOBAL, AuthConfig}
),
{ok, [#{provider := emqx_authn_redis, state := State}]} {ok, [#{provider := emqx_authn_redis, state := State}]} =
= emqx_authentication:list_authenticators(?GLOBAL), emqx_authentication:list_authenticators(?GLOBAL),
{ok, _} = emqx_authn_redis:authenticate( {ok, _} = emqx_authn_redis:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">> username => <<"plain">>,
}, password => <<"plain">>
State), },
State
),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
% Authenticator should not be usable anymore % Authenticator should not be usable anymore
?assertMatch( ?assertMatch(
ignore, ignore,
emqx_authn_redis:authenticate( emqx_authn_redis:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">> username => <<"plain">>,
}, password => <<"plain">>
State)). },
State
)
).
t_update(_Config) -> t_update(_Config) ->
CorrectConfig = raw_redis_auth_config(), CorrectConfig = raw_redis_auth_config(),
IncorrectConfig = IncorrectConfig =
CorrectConfig#{ CorrectConfig#{
cmd => <<"HMGET invalid_key:${username} password_hash salt is_superuser">>}, cmd => <<"HMGET invalid_key:${username} password_hash salt is_superuser">>
},
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, IncorrectConfig}), {create_authenticator, ?GLOBAL, IncorrectConfig}
),
{error, not_authorized} = emqx_access_control:authenticate( {error, not_authorized} = emqx_access_control:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}), protocol => mqtt
}
),
% We update with config with correct query, provider should update and work properly % We update with config with correct query, provider should update and work properly
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{update_authenticator, ?GLOBAL, <<"password_based:redis">>, CorrectConfig}), {update_authenticator, ?GLOBAL, <<"password_based:redis">>, CorrectConfig}
),
{ok,_} = emqx_access_control:authenticate( {ok, _} = emqx_access_control:authenticate(
#{username => <<"plain">>, #{
password => <<"plain">>, username => <<"plain">>,
listener => 'tcp:default', password => <<"plain">>,
protocol => mqtt listener => 'tcp:default',
}). protocol => mqtt
}
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -222,194 +252,218 @@ t_update(_Config) ->
raw_redis_auth_config() -> raw_redis_auth_config() ->
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
password_hash_algorithm => #{name => <<"plain">>, password_hash_algorithm => #{
salt_position => <<"suffix">>}, name => <<"plain">>,
enable => <<"true">>, salt_position => <<"suffix">>
},
enable => <<"true">>,
backend => <<"redis">>, backend => <<"redis">>,
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>, cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
database => <<"1">>, database => <<"1">>,
password => <<"public">>, password => <<"public">>,
server => redis_server() server => redis_server()
}. }.
user_seeds() -> user_seeds() ->
[#{data => #{ [
password_hash => <<"plainsalt">>, #{
salt => <<"salt">>, data => #{
is_superuser => <<"1">> password_hash => <<"plainsalt">>,
}, salt => <<"salt">>,
credentials => #{ is_superuser => <<"1">>
username => <<"plain">>, },
password => <<"plain">>}, credentials => #{
key => <<"mqtt_user:plain">>, username => <<"plain">>,
config_params => #{}, password => <<"plain">>
result => {ok,#{is_superuser => true}} },
}, key => <<"mqtt_user:plain">>,
config_params => #{},
#{data => #{ result => {ok, #{is_superuser => true}}
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"salt">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"md5">>,
password => <<"md5">>
},
key => <<"mqtt_user:md5">>,
config_params => #{
password_hash_algorithm => #{name => <<"md5">>,
salt_position => <<"suffix">>}
},
result => {ok,#{is_superuser => false}}
},
#{data => #{
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
salt => <<"salt">>,
is_superuser => <<"1">>
}, },
credentials => #{
clientid => <<"sha256">>,
password => <<"sha256">>
},
key => <<"mqtt_user:sha256">>,
config_params => #{
cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"sha256">>,
salt_position => <<"prefix">>}
},
result => {ok,#{is_superuser => true}}
},
#{data => #{ #{
password_hash => data => #{
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, salt => <<"salt">>,
is_superuser => <<"0">> is_superuser => <<"0">>
}, },
credentials => #{ credentials => #{
username => <<"bcrypt">>, username => <<"md5">>,
password => <<"bcrypt">> password => <<"md5">>
}, },
key => <<"mqtt_user:bcrypt">>, key => <<"mqtt_user:md5">>,
config_params => #{ config_params => #{
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{
}, name => <<"md5">>,
result => {ok,#{is_superuser => false}} salt_position => <<"suffix">>
}, }
#{data => #{ },
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>, result => {ok, #{is_superuser => false}}
salt => <<"ATHENA.MIT.EDUraeburn">>, },
is_superuser => <<"0">>
},
credentials => #{
username => <<"pbkdf2">>,
password => <<"password">>
},
key => <<"mqtt_user:pbkdf2">>,
config_params => #{
password_hash_algorithm => #{name => <<"pbkdf2">>,
iterations => 2,
mac_fun => sha
}
},
result => {ok,#{is_superuser => false}}
},
#{data => #{
password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt0">>,
password => <<"bcrypt">>
},
key => <<"mqtt_user:bcrypt0">>,
config_params => #{
% clientid variable & username credentials
cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error,not_authorized}
},
#{data => #{ #{
password_hash => data => #{
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, password_hash =>
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
is_superuser => <<"0">> salt => <<"salt">>,
}, is_superuser => <<"1">>
credentials => #{ },
username => <<"bcrypt1">>, credentials => #{
password => <<"bcrypt">> clientid => <<"sha256">>,
}, password => <<"sha256">>
key => <<"mqtt_user:bcrypt1">>, },
config_params => #{ key => <<"mqtt_user:sha256">>,
% Bad key in cmd config_params => #{
cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>, cmd => <<"HMGET mqtt_user:${clientid} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"bcrypt">>} password_hash_algorithm => #{
}, name => <<"sha256">>,
result => {error,not_authorized} salt_position => <<"prefix">>
}, }
},
result => {ok, #{is_superuser => true}}
},
#{data => #{ #{
password_hash => data => #{
password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">> is_superuser => <<"0">>
}, },
credentials => #{ credentials => #{
username => <<"bcrypt2">>, username => <<"bcrypt">>,
% Wrong password password => <<"bcrypt">>
password => <<"wrongpass">> },
}, key => <<"mqtt_user:bcrypt">>,
key => <<"mqtt_user:bcrypt2">>, config_params => #{
config_params => #{ password_hash_algorithm => #{name => <<"bcrypt">>}
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>, },
password_hash_algorithm => #{name => <<"bcrypt">>} result => {ok, #{is_superuser => false}}
}, },
result => {error,bad_username_or_password} #{
} data => #{
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
salt => <<"ATHENA.MIT.EDUraeburn">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"pbkdf2">>,
password => <<"password">>
},
key => <<"mqtt_user:pbkdf2">>,
config_params => #{
password_hash_algorithm => #{
name => <<"pbkdf2">>,
iterations => 2,
mac_fun => sha
}
},
result => {ok, #{is_superuser => false}}
},
#{
data => #{
password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt0">>,
password => <<"bcrypt">>
},
key => <<"mqtt_user:bcrypt0">>,
config_params => #{
% clientid variable & username credentials
cmd => <<"HMGET mqtt_client:${clientid} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, not_authorized}
},
#{
data => #{
password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt1">>,
password => <<"bcrypt">>
},
key => <<"mqtt_user:bcrypt1">>,
config_params => #{
% Bad key in cmd
cmd => <<"HMGET badkey:${username} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, not_authorized}
},
#{
data => #{
password_hash =>
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
is_superuser => <<"0">>
},
credentials => #{
username => <<"bcrypt2">>,
% Wrong password
password => <<"wrongpass">>
},
key => <<"mqtt_user:bcrypt2">>,
config_params => #{
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
password_hash_algorithm => #{name => <<"bcrypt">>}
},
result => {error, bad_username_or_password}
}
]. ].
init_seeds() -> init_seeds() ->
ok = drop_seeds(), ok = drop_seeds(),
lists:foreach( lists:foreach(
fun(#{key := UserKey, data := Values}) -> fun(#{key := UserKey, data := Values}) ->
lists:foreach(fun({Key, Value}) -> lists:foreach(
q(["HSET", UserKey, atom_to_list(Key), Value]) fun({Key, Value}) ->
end, q(["HSET", UserKey, atom_to_list(Key), Value])
maps:to_list(Values)) end,
end, maps:to_list(Values)
user_seeds()). )
end,
user_seeds()
).
q(Command) -> q(Command) ->
emqx_resource:query( emqx_resource:query(
?REDIS_RESOURCE, ?REDIS_RESOURCE,
{cmd, Command}). {cmd, Command}
).
drop_seeds() -> drop_seeds() ->
lists:foreach( lists:foreach(
fun(#{key := UserKey}) -> fun(#{key := UserKey}) ->
q(["DEL", UserKey]) q(["DEL", UserKey])
end, end,
user_seeds()). user_seeds()
).
redis_server() -> redis_server() ->
iolist_to_binary(io_lib:format("~s",[?REDIS_HOST])). iolist_to_binary(io_lib:format("~s", [?REDIS_HOST])).
redis_config() -> redis_config() ->
#{auto_reconnect => true, #{
database => 1, auto_reconnect => true,
pool_size => 8, database => 1,
redis_type => single, pool_size => 8,
password => "public", redis_type => single,
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT}, password => "public",
ssl => #{enable => false} server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
}. ssl => #{enable => false}
}.
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
emqx_authentication:initialize_authentication(?GLOBAL, []), emqx_authentication:initialize_authentication(?GLOBAL, []),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
Config. Config.
init_per_suite(Config) -> init_per_suite(Config) ->
@ -56,8 +57,9 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
@ -67,39 +69,55 @@ end_per_suite(_Config) ->
t_create(_Config) -> t_create(_Config) ->
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_redis_auth_with_ssl_opts( create_redis_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.3">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]})). <<"versions">> => [<<"tlsv1.3">>],
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]
}
)
).
t_create_invalid(_Config) -> t_create_invalid(_Config) ->
%% invalid server_name %% invalid server_name
?assertMatch( ?assertMatch(
{ok, _}, {ok, _},
create_redis_auth_with_ssl_opts( create_redis_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server-unknown-host">>,
<<"versions">> => [<<"tlsv1.3">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]})), <<"versions">> => [<<"tlsv1.3">>],
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]
}
)
),
%% incompatible versions %% incompatible versions
?assertMatch( ?assertMatch(
{error, _}, {error, _},
create_redis_auth_with_ssl_opts( create_redis_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.1">>, <<"tlsv1.2">>]})), <<"verify">> => <<"verify_peer">>,
<<"versions">> => [<<"tlsv1.1">>, <<"tlsv1.2">>]
}
)
),
%% incompatible ciphers %% incompatible ciphers
?assertMatch( ?assertMatch(
{error, _}, {error, _},
create_redis_auth_with_ssl_opts( create_redis_auth_with_ssl_opts(
#{<<"server_name_indication">> => <<"authn-server">>, #{
<<"verify">> => <<"verify_peer">>, <<"server_name_indication">> => <<"authn-server">>,
<<"versions">> => [<<"tlsv1.3">>], <<"verify">> => <<"verify_peer">>,
<<"ciphers">> => [<<"TLS_AES_128_GCM_SHA256">>]})). <<"versions">> => [<<"tlsv1.3">>],
<<"ciphers">> => [<<"TLS_AES_128_GCM_SHA256">>]
}
)
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
@ -111,24 +129,27 @@ create_redis_auth_with_ssl_opts(SpecificSSLOpts) ->
raw_redis_auth_config(SpecificSSLOpts) -> raw_redis_auth_config(SpecificSSLOpts) ->
SSLOpts = maps:merge( SSLOpts = maps:merge(
emqx_authn_test_lib:client_ssl_cert_opts(), emqx_authn_test_lib:client_ssl_cert_opts(),
#{enable => <<"true">>}), #{enable => <<"true">>}
),
#{ #{
mechanism => <<"password_based">>, mechanism => <<"password_based">>,
password_hash_algorithm => #{name => <<"plain">>, password_hash_algorithm => #{
salt_position => <<"suffix">>}, name => <<"plain">>,
enable => <<"true">>, salt_position => <<"suffix">>
},
enable => <<"true">>,
backend => <<"redis">>, backend => <<"redis">>,
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>, cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
database => <<"1">>, database => <<"1">>,
password => <<"public">>, password => <<"public">>,
server => redis_server(), server => redis_server(),
ssl => maps:merge(SSLOpts, SpecificSSLOpts) ssl => maps:merge(SSLOpts, SpecificSSLOpts)
}. }.
redis_server() -> redis_server() ->
iolist_to_binary(io_lib:format("~s:~b",[?REDIS_HOST, ?REDIS_TLS_PORT])). iolist_to_binary(io_lib:format("~s:~b", [?REDIS_HOST, ?REDIS_TLS_PORT])).
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -36,16 +36,19 @@ jwt_example() ->
delete_authenticators(Path, Chain) -> delete_authenticators(Path, Chain) ->
case emqx_authentication:list_authenticators(Chain) of case emqx_authentication:list_authenticators(Chain) of
{error, _} -> ok; {error, _} ->
ok;
{ok, Authenticators} -> {ok, Authenticators} ->
lists:foreach( lists:foreach(
fun(#{id := ID}) -> fun(#{id := ID}) ->
emqx:update_config( emqx:update_config(
Path, Path,
{delete_authenticator, Chain, ID}, {delete_authenticator, Chain, ID},
#{rawconf_with_defaults => true}) #{rawconf_with_defaults => true}
)
end, end,
Authenticators) Authenticators
)
end. end.
delete_config(ID) -> delete_config(ID) ->
@ -53,10 +56,13 @@ delete_config(ID) ->
emqx:update_config( emqx:update_config(
[authentication], [authentication],
{delete_authenticator, ?GLOBAL, ID}, {delete_authenticator, ?GLOBAL, ID},
#{rawconf_with_defaults => false}). #{rawconf_with_defaults => false}
).
client_ssl_cert_opts() -> client_ssl_cert_opts() ->
Dir = code:lib_dir(emqx_authn, test), Dir = code:lib_dir(emqx_authn, test),
#{keyfile => filename:join([Dir, "data/certs", "client.key"]), #{
certfile => filename:join([Dir, "data/certs", "client.crt"]), keyfile => filename:join([Dir, "data/certs", "client.key"]),
cacertfile => filename:join([Dir, "data/certs", "ca.crt"])}. certfile => filename:join([Dir, "data/certs", "client.crt"]),
cacertfile => filename:join([Dir, "data/certs", "ca.crt"])
}.

View File

@ -26,14 +26,16 @@
-define(PATH, [authentication]). -define(PATH, [authentication]).
-define(USER_MAP, #{user_id := _, -define(USER_MAP, #{
is_superuser := _}). user_id := _,
is_superuser := _
}).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
_ = application:load(emqx_conf), _ = application:load(emqx_conf),
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
Config. Config.
@ -44,8 +46,9 @@ init_per_testcase(_Case, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
mria:clear_table(emqx_enhanced_authn_scram_mnesia), mria:clear_table(emqx_enhanced_authn_scram_mnesia),
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
[authentication], [authentication],
?GLOBAL), ?GLOBAL
),
Config. Config.
end_per_testcase(_Case, Config) -> end_per_testcase(_Case, Config) ->
@ -64,23 +67,25 @@ t_create(_Config) ->
}, },
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, ValidConfig}), {create_authenticator, ?GLOBAL, ValidConfig}
),
{ok, [#{provider := emqx_enhanced_authn_scram_mnesia}]} {ok, [#{provider := emqx_enhanced_authn_scram_mnesia}]} =
= emqx_authentication:list_authenticators(?GLOBAL). emqx_authentication:list_authenticators(?GLOBAL).
t_create_invalid(_Config) -> t_create_invalid(_Config) ->
InvalidConfig = #{ InvalidConfig = #{
<<"mechanism">> => <<"scram">>, <<"mechanism">> => <<"scram">>,
<<"backend">> => <<"built_in_database">>, <<"backend">> => <<"built_in_database">>,
<<"algorithm">> => <<"sha271828">>, <<"algorithm">> => <<"sha271828">>,
<<"iteration_count">> => <<"4096">> <<"iteration_count">> => <<"4096">>
}, },
{error, _} = emqx:update_config( {error, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, InvalidConfig}), {create_authenticator, ?GLOBAL, InvalidConfig}
),
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL). {ok, []} = emqx_authentication:list_authenticators(?GLOBAL).
@ -96,39 +101,47 @@ t_authenticate(_Config) ->
ClientFirstMessage = esasl_scram:client_first_message(Username), ClientFirstMessage = esasl_scram:client_first_message(Username),
ConnectPacket = ?CONNECT_PACKET( ConnectPacket = ?CONNECT_PACKET(
#mqtt_packet_connect{ #mqtt_packet_connect{
proto_ver = ?MQTT_PROTO_V5, proto_ver = ?MQTT_PROTO_V5,
properties = #{ properties = #{
'Authentication-Method' => <<"SCRAM-SHA-512">>, 'Authentication-Method' => <<"SCRAM-SHA-512">>,
'Authentication-Data' => ClientFirstMessage 'Authentication-Data' => ClientFirstMessage
} }
}), }
),
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket), ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
?AUTH_PACKET( ?AUTH_PACKET(
?RC_CONTINUE_AUTHENTICATION, ?RC_CONTINUE_AUTHENTICATION,
#{'Authentication-Data' := ServerFirstMessage}) = receive_packet(), #{'Authentication-Data' := ServerFirstMessage}
) = receive_packet(),
{continue, ClientFinalMessage, ClientCache} = {continue, ClientFinalMessage, ClientCache} =
esasl_scram:check_server_first_message( esasl_scram:check_server_first_message(
ServerFirstMessage, ServerFirstMessage,
#{client_first_message => ClientFirstMessage, #{
password => Password, client_first_message => ClientFirstMessage,
algorithm => Algorithm} password => Password,
algorithm => Algorithm
}
), ),
AuthContinuePacket = ?AUTH_PACKET( AuthContinuePacket = ?AUTH_PACKET(
?RC_CONTINUE_AUTHENTICATION, ?RC_CONTINUE_AUTHENTICATION,
#{'Authentication-Method' => <<"SCRAM-SHA-512">>, #{
'Authentication-Data' => ClientFinalMessage}), 'Authentication-Method' => <<"SCRAM-SHA-512">>,
'Authentication-Data' => ClientFinalMessage
}
),
ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket), ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket),
?CONNACK_PACKET( ?CONNACK_PACKET(
?RC_SUCCESS, ?RC_SUCCESS,
_, _,
#{'Authentication-Data' := ServerFinalMessage}) = receive_packet(), #{'Authentication-Data' := ServerFinalMessage}
) = receive_packet(),
ok = esasl_scram:check_server_final_message( ok = esasl_scram:check_server_final_message(
ServerFinalMessage, ClientCache#{algorithm => Algorithm} ServerFinalMessage, ClientCache#{algorithm => Algorithm}
@ -146,13 +159,14 @@ t_authenticate_bad_username(_Config) ->
ClientFirstMessage = esasl_scram:client_first_message(<<"badusername">>), ClientFirstMessage = esasl_scram:client_first_message(<<"badusername">>),
ConnectPacket = ?CONNECT_PACKET( ConnectPacket = ?CONNECT_PACKET(
#mqtt_packet_connect{ #mqtt_packet_connect{
proto_ver = ?MQTT_PROTO_V5, proto_ver = ?MQTT_PROTO_V5,
properties = #{ properties = #{
'Authentication-Method' => <<"SCRAM-SHA-512">>, 'Authentication-Method' => <<"SCRAM-SHA-512">>,
'Authentication-Data' => ClientFirstMessage 'Authentication-Data' => ClientFirstMessage
} }
}), }
),
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket), ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
@ -170,32 +184,39 @@ t_authenticate_bad_password(_Config) ->
ClientFirstMessage = esasl_scram:client_first_message(Username), ClientFirstMessage = esasl_scram:client_first_message(Username),
ConnectPacket = ?CONNECT_PACKET( ConnectPacket = ?CONNECT_PACKET(
#mqtt_packet_connect{ #mqtt_packet_connect{
proto_ver = ?MQTT_PROTO_V5, proto_ver = ?MQTT_PROTO_V5,
properties = #{ properties = #{
'Authentication-Method' => <<"SCRAM-SHA-512">>, 'Authentication-Method' => <<"SCRAM-SHA-512">>,
'Authentication-Data' => ClientFirstMessage 'Authentication-Data' => ClientFirstMessage
} }
}), }
),
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket), ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
?AUTH_PACKET( ?AUTH_PACKET(
?RC_CONTINUE_AUTHENTICATION, ?RC_CONTINUE_AUTHENTICATION,
#{'Authentication-Data' := ServerFirstMessage}) = receive_packet(), #{'Authentication-Data' := ServerFirstMessage}
) = receive_packet(),
{continue, ClientFinalMessage, _ClientCache} = {continue, ClientFinalMessage, _ClientCache} =
esasl_scram:check_server_first_message( esasl_scram:check_server_first_message(
ServerFirstMessage, ServerFirstMessage,
#{client_first_message => ClientFirstMessage, #{
password => <<"badpassword">>, client_first_message => ClientFirstMessage,
algorithm => Algorithm} password => <<"badpassword">>,
algorithm => Algorithm
}
), ),
AuthContinuePacket = ?AUTH_PACKET( AuthContinuePacket = ?AUTH_PACKET(
?RC_CONTINUE_AUTHENTICATION, ?RC_CONTINUE_AUTHENTICATION,
#{'Authentication-Method' => <<"SCRAM-SHA-512">>, #{
'Authentication-Data' => ClientFinalMessage}), 'Authentication-Method' => <<"SCRAM-SHA-512">>,
'Authentication-Data' => ClientFinalMessage
}
),
ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket), ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket),
@ -218,7 +239,7 @@ t_destroy(_) ->
ok = emqx_enhanced_authn_scram_mnesia:destroy(State0), ok = emqx_enhanced_authn_scram_mnesia:destroy(State0),
{ok, State1} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), {ok, State1} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config),
{error,not_found} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State1), {error, not_found} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State1),
{ok, _} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, StateOther). {ok, _} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, StateOther).
t_add_user(_) -> t_add_user(_) ->
@ -248,12 +269,14 @@ t_update_user(_) ->
{ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State), {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State),
{ok, #{is_superuser := false}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State), {ok, #{is_superuser := false}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State),
{ok, {ok, #{
#{user_id := <<"u">>, user_id := <<"u">>,
is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:update_user( is_superuser := true
<<"u">>, }} = emqx_enhanced_authn_scram_mnesia:update_user(
#{password => <<"p1">>, is_superuser => true}, <<"u">>,
State), #{password => <<"p1">>, is_superuser => true},
State
),
{ok, #{is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State). {ok, #{is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State).
@ -261,29 +284,47 @@ t_list_users(_) ->
Config = config(), Config = config(),
{ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), {ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config),
Users = [#{user_id => <<"u1">>, password => <<"p">>}, Users = [
#{user_id => <<"u2">>, password => <<"p">>}, #{user_id => <<"u1">>, password => <<"p">>},
#{user_id => <<"u3">>, password => <<"p">>}], #{user_id => <<"u2">>, password => <<"p">>},
#{user_id => <<"u3">>, password => <<"p">>}
],
lists:foreach( lists:foreach(
fun(U) -> {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(U, State) end, fun(U) -> {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(U, State) end,
Users), Users
),
#{data := [?USER_MAP, ?USER_MAP], #{
meta := #{page := 1, limit := 2, count := 3}} = emqx_enhanced_authn_scram_mnesia:list_users( data := [?USER_MAP, ?USER_MAP],
#{<<"page">> => 1, <<"limit">> => 2}, meta := #{page := 1, limit := 2, count := 3}
State), } = emqx_enhanced_authn_scram_mnesia:list_users(
#{data := [?USER_MAP], #{<<"page">> => 1, <<"limit">> => 2},
meta := #{page := 2, limit := 2, count := 3}} = emqx_enhanced_authn_scram_mnesia:list_users( State
#{<<"page">> => 2, <<"limit">> => 2}, ),
State), #{
#{data := [#{user_id := <<"u1">>, data := [?USER_MAP],
is_superuser := _}], meta := #{page := 2, limit := 2, count := 3}
meta := #{page := 1, limit := 3, count := 1}} = emqx_enhanced_authn_scram_mnesia:list_users( } = emqx_enhanced_authn_scram_mnesia:list_users(
#{ <<"page">> => 1 #{<<"page">> => 2, <<"limit">> => 2},
, <<"limit">> => 3 State
, <<"like_username">> => <<"1">>}, ),
State). #{
data := [
#{
user_id := <<"u1">>,
is_superuser := _
}
],
meta := #{page := 1, limit := 3, count := 1}
} = emqx_enhanced_authn_scram_mnesia:list_users(
#{
<<"page">> => 1,
<<"limit">> => 3,
<<"like_username">> => <<"1">>
},
State
).
t_is_superuser(_Config) -> t_is_superuser(_Config) ->
ok = test_is_superuser(#{is_superuser => false}, false), ok = test_is_superuser(#{is_superuser => false}, false),
@ -297,36 +338,44 @@ test_is_superuser(UserInfo, ExpectedIsSuperuser) ->
Username = <<"u">>, Username = <<"u">>,
Password = <<"p">>, Password = <<"p">>,
UserInfo0 = UserInfo#{user_id => Username, UserInfo0 = UserInfo#{
password => Password}, user_id => Username,
password => Password
},
{ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(UserInfo0, State), {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(UserInfo0, State),
ClientFirstMessage = esasl_scram:client_first_message(Username), ClientFirstMessage = esasl_scram:client_first_message(Username),
{continue, ServerFirstMessage, ServerCache} {continue, ServerFirstMessage, ServerCache} =
= emqx_enhanced_authn_scram_mnesia:authenticate( emqx_enhanced_authn_scram_mnesia:authenticate(
#{auth_method => <<"SCRAM-SHA-512">>, #{
auth_data => ClientFirstMessage, auth_method => <<"SCRAM-SHA-512">>,
auth_cache => #{} auth_data => ClientFirstMessage,
}, auth_cache => #{}
State), },
State
),
{continue, ClientFinalMessage, ClientCache} = {continue, ClientFinalMessage, ClientCache} =
esasl_scram:check_server_first_message( esasl_scram:check_server_first_message(
ServerFirstMessage, ServerFirstMessage,
#{client_first_message => ClientFirstMessage, #{
password => Password, client_first_message => ClientFirstMessage,
algorithm => sha512} password => Password,
algorithm => sha512
}
), ),
{ok, UserInfo1, ServerFinalMessage} {ok, UserInfo1, ServerFinalMessage} =
= emqx_enhanced_authn_scram_mnesia:authenticate( emqx_enhanced_authn_scram_mnesia:authenticate(
#{auth_method => <<"SCRAM-SHA-512">>, #{
auth_data => ClientFinalMessage, auth_method => <<"SCRAM-SHA-512">>,
auth_cache => ServerCache auth_data => ClientFinalMessage,
}, auth_cache => ServerCache
State), },
State
),
ok = esasl_scram:check_server_final_message( ok = esasl_scram:check_server_final_message(
ServerFinalMessage, ClientCache#{algorithm => sha512} ServerFinalMessage, ClientCache#{algorithm => sha512}
@ -336,18 +385,17 @@ test_is_superuser(UserInfo, ExpectedIsSuperuser) ->
ok = emqx_enhanced_authn_scram_mnesia:destroy(State). ok = emqx_enhanced_authn_scram_mnesia:destroy(State).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
config() -> config() ->
#{ #{
mechanism => <<"scram">>, mechanism => <<"scram">>,
backend => <<"built_in_database">>, backend => <<"built_in_database">>,
algorithm => sha512, algorithm => sha512,
iteration_count => 4096 iteration_count => 4096
}. }.
raw_config(Algorithm) -> raw_config(Algorithm) ->
#{ #{
@ -361,14 +409,16 @@ init_auth(Username, Password, Algorithm) ->
Config = raw_config(Algorithm), Config = raw_config(Algorithm),
{ok, _} = emqx:update_config( {ok, _} = emqx:update_config(
?PATH, ?PATH,
{create_authenticator, ?GLOBAL, Config}), {create_authenticator, ?GLOBAL, Config}
),
{ok, [#{state := State}]} = emqx_authentication:list_authenticators(?GLOBAL), {ok, [#{state := State}]} = emqx_authentication:list_authenticators(?GLOBAL),
emqx_enhanced_authn_scram_mnesia:add_user( emqx_enhanced_authn_scram_mnesia:add_user(
#{user_id => Username, password => Password}, #{user_id => Username, password => Password},
State). State
).
receive_packet() -> receive_packet() ->
receive receive