style: erlfmt apps/emqx_authn
This commit is contained in:
parent
9ffc58071d
commit
aae2d01582
|
@ -1,23 +1,32 @@
|
|||
%% -*- mode: erlang -*-
|
||||
|
||||
{deps,
|
||||
[ {emqx, {path, "../emqx"}}
|
||||
, {emqx_connector, {path, "../emqx_connector"}}
|
||||
]}.
|
||||
{deps, [
|
||||
{emqx, {path, "../emqx"}},
|
||||
{emqx_connector, {path, "../emqx_connector"}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warnings_as_errors,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
{parse_transform}]}.
|
||||
{erl_opts, [
|
||||
warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warnings_as_errors,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
{parse_transform}
|
||||
]}.
|
||||
|
||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||
locals_not_used, deprecated_function_calls,
|
||||
warnings_as_errors, deprecated_functions]}.
|
||||
{xref_checks, [
|
||||
undefined_function_calls,
|
||||
undefined_functions,
|
||||
locals_not_used,
|
||||
deprecated_function_calls,
|
||||
warnings_as_errors,
|
||||
deprecated_functions
|
||||
]}.
|
||||
|
||||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
||||
|
||||
{project_plugins, [erlfmt]}.
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{application, emqx_authn,
|
||||
[{description, "EMQX Authentication"},
|
||||
{vsn, "0.1.0"},
|
||||
{modules, []},
|
||||
{registered, [emqx_authn_sup, emqx_authn_registry]},
|
||||
{applications, [kernel,stdlib,emqx_resource,ehttpc,epgsql,mysql,jose]},
|
||||
{mod, {emqx_authn_app,[]}},
|
||||
{env, []},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"}]}
|
||||
]}.
|
||||
{application, emqx_authn, [
|
||||
{description, "EMQX Authentication"},
|
||||
{vsn, "0.1.0"},
|
||||
{modules, []},
|
||||
{registered, [emqx_authn_sup, emqx_authn_registry]},
|
||||
{applications, [kernel, stdlib, emqx_resource, ehttpc, epgsql, mysql, jose]},
|
||||
{mod, {emqx_authn_app, []}},
|
||||
{env, []},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"}]}
|
||||
]}.
|
||||
|
|
|
@ -16,28 +16,31 @@
|
|||
|
||||
-module(emqx_authn).
|
||||
|
||||
-export([ providers/0
|
||||
, check_config/1
|
||||
, check_config/2
|
||||
, check_configs/1
|
||||
]).
|
||||
-export([
|
||||
providers/0,
|
||||
check_config/1,
|
||||
check_config/2,
|
||||
check_configs/1
|
||||
]).
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
|
||||
providers() ->
|
||||
[ {{'password_based', 'built_in_database'}, emqx_authn_mnesia}
|
||||
, {{'password_based', mysql}, emqx_authn_mysql}
|
||||
, {{'password_based', postgresql}, emqx_authn_pgsql}
|
||||
, {{'password_based', mongodb}, emqx_authn_mongodb}
|
||||
, {{'password_based', redis}, emqx_authn_redis}
|
||||
, {{'password_based', 'http'}, emqx_authn_http}
|
||||
, {jwt, emqx_authn_jwt}
|
||||
, {{scram, 'built_in_database'}, emqx_enhanced_authn_scram_mnesia}
|
||||
[
|
||||
{{'password_based', 'built_in_database'}, emqx_authn_mnesia},
|
||||
{{'password_based', mysql}, emqx_authn_mysql},
|
||||
{{'password_based', postgresql}, emqx_authn_pgsql},
|
||||
{{'password_based', mongodb}, emqx_authn_mongodb},
|
||||
{{'password_based', redis}, emqx_authn_redis},
|
||||
{{'password_based', 'http'}, emqx_authn_http},
|
||||
{jwt, emqx_authn_jwt},
|
||||
{{scram, 'built_in_database'}, emqx_enhanced_authn_scram_mnesia}
|
||||
].
|
||||
|
||||
check_configs(C) when is_map(C) ->
|
||||
check_configs([C]);
|
||||
check_configs([]) -> [];
|
||||
check_configs([]) ->
|
||||
[];
|
||||
check_configs([Config | Configs]) ->
|
||||
[check_config(Config) | check_configs(Configs)].
|
||||
|
||||
|
@ -51,22 +54,26 @@ check_config(Config, Opts) ->
|
|||
end.
|
||||
|
||||
do_check_config(#{<<"mechanism">> := Mec} = Config, Opts) ->
|
||||
Key = case maps:get(<<"backend">>, Config, false) of
|
||||
false -> atom(Mec);
|
||||
Backend -> {atom(Mec), atom(Backend)}
|
||||
end,
|
||||
Key =
|
||||
case maps:get(<<"backend">>, Config, false) of
|
||||
false -> atom(Mec);
|
||||
Backend -> {atom(Mec), atom(Backend)}
|
||||
end,
|
||||
case lists:keyfind(Key, 1, providers()) of
|
||||
false ->
|
||||
throw({unknown_handler, Key});
|
||||
{_, ProviderModule} ->
|
||||
hocon_tconf:check_plain(ProviderModule, #{?CONF_NS_BINARY => Config},
|
||||
Opts#{atom_key => true})
|
||||
hocon_tconf:check_plain(
|
||||
ProviderModule,
|
||||
#{?CONF_NS_BINARY => Config},
|
||||
Opts#{atom_key => true}
|
||||
)
|
||||
end.
|
||||
|
||||
atom(Bin) ->
|
||||
try
|
||||
binary_to_existing_atom(Bin, utf8)
|
||||
catch
|
||||
_ : _ ->
|
||||
_:_ ->
|
||||
throw({unknown_auth_provider, Bin})
|
||||
end.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -21,9 +21,10 @@
|
|||
-behaviour(application).
|
||||
|
||||
%% Application callbacks
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
]).
|
||||
-export([
|
||||
start/2,
|
||||
stop/1
|
||||
]).
|
||||
|
||||
-include_lib("emqx/include/emqx_authentication.hrl").
|
||||
|
||||
|
@ -51,13 +52,15 @@ initialize() ->
|
|||
ok = ?AUTHN:register_providers(emqx_authn:providers()),
|
||||
|
||||
lists:foreach(
|
||||
fun({ChainName, RawAuthConfigs}) ->
|
||||
AuthConfig = emqx_authn:check_configs(RawAuthConfigs),
|
||||
?AUTHN:initialize_authentication(
|
||||
ChainName,
|
||||
AuthConfig)
|
||||
end,
|
||||
chain_configs()).
|
||||
fun({ChainName, RawAuthConfigs}) ->
|
||||
AuthConfig = emqx_authn:check_configs(RawAuthConfigs),
|
||||
?AUTHN:initialize_authentication(
|
||||
ChainName,
|
||||
AuthConfig
|
||||
)
|
||||
end,
|
||||
chain_configs()
|
||||
).
|
||||
|
||||
deinitialize() ->
|
||||
ok = ?AUTHN:deregister_providers(provider_types()),
|
||||
|
@ -71,15 +74,16 @@ global_chain_config() ->
|
|||
|
||||
listener_chain_configs() ->
|
||||
lists:map(
|
||||
fun({ListenerID, _}) ->
|
||||
{ListenerID, emqx:get_raw_config(auth_config_path(ListenerID), [])}
|
||||
end,
|
||||
emqx_listeners:list()).
|
||||
fun({ListenerID, _}) ->
|
||||
{ListenerID, emqx:get_raw_config(auth_config_path(ListenerID), [])}
|
||||
end,
|
||||
emqx_listeners:list()
|
||||
).
|
||||
|
||||
auth_config_path(ListenerID) ->
|
||||
[<<"listeners">>]
|
||||
++ binary:split(atom_to_binary(ListenerID), <<":">>)
|
||||
++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY].
|
||||
[<<"listeners">>] ++
|
||||
binary:split(atom_to_binary(ListenerID), <<":">>) ++
|
||||
[?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY].
|
||||
|
||||
provider_types() ->
|
||||
lists:map(fun({Type, _Module}) -> Type end, emqx_authn:providers()).
|
||||
|
|
|
@ -18,21 +18,25 @@
|
|||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-type(simple_algorithm_name() :: plain | md5 | sha | sha256 | sha512).
|
||||
-type(salt_position() :: prefix | suffix).
|
||||
-type simple_algorithm_name() :: plain | md5 | sha | sha256 | sha512.
|
||||
-type salt_position() :: prefix | suffix.
|
||||
|
||||
-type(simple_algorithm() :: #{name := simple_algorithm_name(),
|
||||
salt_position := salt_position()}).
|
||||
-type simple_algorithm() :: #{
|
||||
name := simple_algorithm_name(),
|
||||
salt_position := salt_position()
|
||||
}.
|
||||
|
||||
-type(bcrypt_algorithm() :: #{name := bcrypt}).
|
||||
-type(bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}).
|
||||
-type bcrypt_algorithm() :: #{name := bcrypt}.
|
||||
-type bcrypt_algorithm_rw() :: #{name := bcrypt, salt_rounds := integer()}.
|
||||
|
||||
-type(pbkdf2_algorithm() :: #{name := pbkdf2,
|
||||
mac_fun := emqx_passwd:pbkdf2_mac_fun(),
|
||||
iterations := pos_integer()}).
|
||||
-type pbkdf2_algorithm() :: #{
|
||||
name := pbkdf2,
|
||||
mac_fun := emqx_passwd:pbkdf2_mac_fun(),
|
||||
iterations := pos_integer()
|
||||
}.
|
||||
|
||||
-type(algorithm() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm()).
|
||||
-type(algorithm_rw() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm_rw()).
|
||||
-type algorithm() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm().
|
||||
-type algorithm_rw() :: simple_algorithm() | pbkdf2_algorithm() | bcrypt_algorithm_rw().
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
|
@ -40,37 +44,44 @@
|
|||
|
||||
-behaviour(hocon_schema).
|
||||
|
||||
-export([roots/0,
|
||||
fields/1,
|
||||
namespace/0]).
|
||||
-export([
|
||||
roots/0,
|
||||
fields/1,
|
||||
namespace/0
|
||||
]).
|
||||
|
||||
-export([type_ro/1,
|
||||
type_rw/1]).
|
||||
-export([
|
||||
type_ro/1,
|
||||
type_rw/1
|
||||
]).
|
||||
|
||||
-export([init/1,
|
||||
gen_salt/1,
|
||||
hash/2,
|
||||
check_password/4]).
|
||||
-export([
|
||||
init/1,
|
||||
gen_salt/1,
|
||||
hash/2,
|
||||
check_password/4
|
||||
]).
|
||||
|
||||
namespace() -> "authn-hash".
|
||||
roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms].
|
||||
|
||||
fields(bcrypt_rw) ->
|
||||
fields(bcrypt) ++
|
||||
[{salt_rounds, fun salt_rounds/1}];
|
||||
|
||||
[{salt_rounds, fun salt_rounds/1}];
|
||||
fields(bcrypt) ->
|
||||
[{name, {enum, [bcrypt]}}];
|
||||
|
||||
fields(pbkdf2) ->
|
||||
[{name, {enum, [pbkdf2]}},
|
||||
{mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}},
|
||||
{iterations, integer()},
|
||||
{dk_length, fun dk_length/1}];
|
||||
|
||||
[
|
||||
{name, {enum, [pbkdf2]}},
|
||||
{mac_fun, {enum, [md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512]}},
|
||||
{iterations, integer()},
|
||||
{dk_length, fun dk_length/1}
|
||||
];
|
||||
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(desc) -> "Specifies whether the password salt is stored as a prefix or the suffix.";
|
||||
|
@ -89,47 +100,56 @@ dk_length(_) -> undefined.
|
|||
|
||||
type_rw(type) ->
|
||||
hoconsc:union(rw_refs());
|
||||
type_rw(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
|
||||
type_rw(_) -> undefined.
|
||||
type_rw(default) ->
|
||||
#{<<"name">> => sha256, <<"salt_position">> => prefix};
|
||||
type_rw(_) ->
|
||||
undefined.
|
||||
|
||||
type_ro(type) ->
|
||||
hoconsc:union(ro_refs());
|
||||
type_ro(default) -> #{<<"name">> => sha256, <<"salt_position">> => prefix};
|
||||
type_ro(_) -> undefined.
|
||||
type_ro(default) ->
|
||||
#{<<"name">> => sha256, <<"salt_position">> => prefix};
|
||||
type_ro(_) ->
|
||||
undefined.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
-spec(init(algorithm()) -> ok).
|
||||
-spec init(algorithm()) -> ok.
|
||||
init(#{name := bcrypt}) ->
|
||||
{ok, _} = application:ensure_all_started(bcrypt),
|
||||
ok;
|
||||
init(#{name := _Other}) ->
|
||||
ok.
|
||||
|
||||
|
||||
-spec(gen_salt(algorithm_rw()) -> emqx_passwd:salt()).
|
||||
-spec gen_salt(algorithm_rw()) -> emqx_passwd:salt().
|
||||
gen_salt(#{name := plain}) ->
|
||||
<<>>;
|
||||
gen_salt(#{name := bcrypt,
|
||||
salt_rounds := Rounds}) ->
|
||||
gen_salt(#{
|
||||
name := bcrypt,
|
||||
salt_rounds := Rounds
|
||||
}) ->
|
||||
{ok, Salt} = bcrypt:gen_salt(Rounds),
|
||||
list_to_binary(Salt);
|
||||
gen_salt(#{name := Other}) when Other =/= plain, Other =/= bcrypt ->
|
||||
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
||||
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) ->
|
||||
Salt0 = gen_salt(Algorithm),
|
||||
Hash = emqx_passwd:hash({bcrypt, Salt0}, Password),
|
||||
Salt = Hash,
|
||||
{Hash, Salt};
|
||||
hash(#{name := pbkdf2,
|
||||
mac_fun := MacFun,
|
||||
iterations := Iterations} = Algorithm, Password) ->
|
||||
hash(
|
||||
#{
|
||||
name := pbkdf2,
|
||||
mac_fun := MacFun,
|
||||
iterations := Iterations
|
||||
} = Algorithm,
|
||||
Password
|
||||
) ->
|
||||
Salt = gen_salt(Algorithm),
|
||||
DKLength = maps:get(dk_length, Algorithm, undefined),
|
||||
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, Salt}.
|
||||
|
||||
|
||||
-spec(check_password(
|
||||
algorithm(),
|
||||
emqx_passwd:salt(),
|
||||
emqx_passwd:hash(),
|
||||
emqx_passwd:password()) -> boolean()).
|
||||
-spec check_password(
|
||||
algorithm(),
|
||||
emqx_passwd:salt(),
|
||||
emqx_passwd:hash(),
|
||||
emqx_passwd:password()
|
||||
) -> boolean().
|
||||
check_password(#{name := bcrypt}, _Salt, PasswordHash, Password) ->
|
||||
emqx_passwd:check_pass({bcrypt, PasswordHash}, PasswordHash, Password);
|
||||
check_password(#{name := pbkdf2,
|
||||
mac_fun := MacFun,
|
||||
iterations := Iterations} = Algorithm,
|
||||
Salt, PasswordHash, Password) ->
|
||||
check_password(
|
||||
#{
|
||||
name := pbkdf2,
|
||||
mac_fun := MacFun,
|
||||
iterations := Iterations
|
||||
} = Algorithm,
|
||||
Salt,
|
||||
PasswordHash,
|
||||
Password
|
||||
) ->
|
||||
DKLength = maps:get(dk_length, Algorithm, undefined),
|
||||
emqx_passwd:check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, 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() ->
|
||||
[hoconsc:ref(?MODULE, bcrypt_rw),
|
||||
hoconsc:ref(?MODULE, pbkdf2),
|
||||
hoconsc:ref(?MODULE, other_algorithms)].
|
||||
[
|
||||
hoconsc:ref(?MODULE, bcrypt_rw),
|
||||
hoconsc:ref(?MODULE, pbkdf2),
|
||||
hoconsc:ref(?MODULE, other_algorithms)
|
||||
].
|
||||
|
||||
ro_refs() ->
|
||||
[hoconsc:ref(?MODULE, bcrypt),
|
||||
hoconsc:ref(?MODULE, pbkdf2),
|
||||
hoconsc:ref(?MODULE, other_algorithms)].
|
||||
[
|
||||
hoconsc:ref(?MODULE, bcrypt),
|
||||
hoconsc:ref(?MODULE, pbkdf2),
|
||||
hoconsc:ref(?MODULE, other_algorithms)
|
||||
].
|
||||
|
|
|
@ -20,21 +20,20 @@
|
|||
-include_lib("typerefl/include/types.hrl").
|
||||
-import(hoconsc, [mk/2, ref/2]).
|
||||
|
||||
-export([ common_fields/0
|
||||
, roots/0
|
||||
, fields/1
|
||||
, authenticator_type/0
|
||||
, root_type/0
|
||||
, mechanism/1
|
||||
, backend/1
|
||||
]).
|
||||
-export([
|
||||
common_fields/0,
|
||||
roots/0,
|
||||
fields/1,
|
||||
authenticator_type/0,
|
||||
root_type/0,
|
||||
mechanism/1,
|
||||
backend/1
|
||||
]).
|
||||
|
||||
roots() -> [].
|
||||
|
||||
|
||||
common_fields() ->
|
||||
[ {enable, fun enable/1}
|
||||
].
|
||||
[{enable, fun enable/1}].
|
||||
|
||||
enable(type) -> boolean();
|
||||
enable(default) -> true;
|
||||
|
@ -54,46 +53,60 @@ root_type() ->
|
|||
hoconsc:array(authenticator_type()).
|
||||
|
||||
mechanism(Name) ->
|
||||
hoconsc:mk(hoconsc:enum([Name]),
|
||||
#{ required => true
|
||||
, desc => "Authentication mechanism."
|
||||
}).
|
||||
hoconsc:mk(
|
||||
hoconsc:enum([Name]),
|
||||
#{
|
||||
required => true,
|
||||
desc => "Authentication mechanism."
|
||||
}
|
||||
).
|
||||
|
||||
backend(Name) ->
|
||||
hoconsc:mk(hoconsc:enum([Name]),
|
||||
#{ required => true
|
||||
, desc => "Backend type."
|
||||
}).
|
||||
hoconsc:mk(
|
||||
hoconsc:enum([Name]),
|
||||
#{
|
||||
required => true,
|
||||
desc => "Backend type."
|
||||
}
|
||||
).
|
||||
|
||||
fields("metrics_status_fields") ->
|
||||
[ {"metrics", mk(ref(?MODULE, "metrics"), #{desc => "The metrics of the resource"})}
|
||||
, {"node_metrics", mk(hoconsc:array(ref(?MODULE, "node_metrics")),
|
||||
#{ desc => "The metrics 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"
|
||||
})}
|
||||
[
|
||||
{"metrics", mk(ref(?MODULE, "metrics"), #{desc => "The metrics of the resource"})},
|
||||
{"node_metrics",
|
||||
mk(
|
||||
hoconsc:array(ref(?MODULE, "node_metrics")),
|
||||
#{desc => "The metrics 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") ->
|
||||
[ {"matched", mk(integer(), #{desc => "Count of this resource is queried"})}
|
||||
, {"success", mk(integer(), #{desc => "Count of query success"})}
|
||||
, {"failed", mk(integer(), #{desc => "Count of query failed"})}
|
||||
, {"rate", mk(float(), #{desc => "The rate of matched, times/second"})}
|
||||
, {"rate_max", mk(float(), #{desc => "The max rate of matched, times/second"})}
|
||||
, {"rate_last5m", mk(float(),
|
||||
#{desc => "The average rate of matched in the last 5 minutes, times/second"})}
|
||||
[
|
||||
{"matched", mk(integer(), #{desc => "Count of this resource is queried"})},
|
||||
{"success", mk(integer(), #{desc => "Count of query success"})},
|
||||
{"failed", mk(integer(), #{desc => "Count of query failed"})},
|
||||
{"rate", mk(float(), #{desc => "The rate of matched, times/second"})},
|
||||
{"rate_max", mk(float(), #{desc => "The max rate of matched, times/second"})},
|
||||
{"rate_last5m",
|
||||
mk(
|
||||
float(),
|
||||
#{desc => "The average rate of matched in the last 5 minutes, times/second"}
|
||||
)}
|
||||
];
|
||||
|
||||
fields("node_metrics") ->
|
||||
[ node_name()
|
||||
, {"metrics", mk(ref(?MODULE, "metrics"), #{})}
|
||||
[
|
||||
node_name(),
|
||||
{"metrics", mk(ref(?MODULE, "metrics"), #{})}
|
||||
];
|
||||
|
||||
fields("node_status") ->
|
||||
[ node_name()
|
||||
, {"status", mk(status(), #{desc => "Status of the node."})}
|
||||
[
|
||||
node_name(),
|
||||
{"status", mk(status(), #{desc => "Status of the node."})}
|
||||
].
|
||||
|
||||
status() ->
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([ start_link/0
|
||||
, init/1
|
||||
]).
|
||||
-export([
|
||||
start_link/0,
|
||||
init/1
|
||||
]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
|
|
@ -19,26 +19,29 @@
|
|||
-include_lib("emqx/include/emqx_placeholder.hrl").
|
||||
-include_lib("emqx_authn.hrl").
|
||||
|
||||
-export([ check_password_from_selected_map/3
|
||||
, parse_deep/1
|
||||
, parse_str/1
|
||||
, parse_sql/2
|
||||
, render_deep/2
|
||||
, render_str/2
|
||||
, render_sql_params/2
|
||||
, is_superuser/1
|
||||
, bin/1
|
||||
, ensure_apps_started/1
|
||||
, cleanup_resources/0
|
||||
, make_resource_id/1
|
||||
]).
|
||||
-export([
|
||||
check_password_from_selected_map/3,
|
||||
parse_deep/1,
|
||||
parse_str/1,
|
||||
parse_sql/2,
|
||||
render_deep/2,
|
||||
render_str/2,
|
||||
render_sql_params/2,
|
||||
is_superuser/1,
|
||||
bin/1,
|
||||
ensure_apps_started/1,
|
||||
cleanup_resources/0,
|
||||
make_resource_id/1
|
||||
]).
|
||||
|
||||
-define(AUTHN_PLACEHOLDERS, [?PH_USERNAME,
|
||||
?PH_CLIENTID,
|
||||
?PH_PASSWORD,
|
||||
?PH_PEERHOST,
|
||||
?PH_CERT_SUBJECT,
|
||||
?PH_CERT_CN_NAME]).
|
||||
-define(AUTHN_PLACEHOLDERS, [
|
||||
?PH_USERNAME,
|
||||
?PH_CLIENTID,
|
||||
?PH_PASSWORD,
|
||||
?PH_PEERHOST,
|
||||
?PH_CERT_SUBJECT,
|
||||
?PH_CERT_CN_NAME
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% APIs
|
||||
|
@ -47,12 +50,12 @@
|
|||
check_password_from_selected_map(_Algorithm, _Selected, undefined) ->
|
||||
{error, bad_username_or_password};
|
||||
check_password_from_selected_map(
|
||||
Algorithm, #{<<"password_hash">> := Hash} = Selected, Password) ->
|
||||
Algorithm, #{<<"password_hash">> := Hash} = Selected, Password
|
||||
) ->
|
||||
Salt = maps:get(<<"salt">>, Selected, <<>>),
|
||||
case emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
|
||||
true -> ok;
|
||||
false ->
|
||||
{error, bad_username_or_password}
|
||||
false -> {error, bad_username_or_password}
|
||||
end.
|
||||
|
||||
parse_deep(Template) ->
|
||||
|
@ -63,27 +66,33 @@ parse_str(Template) ->
|
|||
|
||||
parse_sql(Template, ReplaceWith) ->
|
||||
emqx_placeholder:preproc_sql(
|
||||
Template,
|
||||
#{replace_with => ReplaceWith,
|
||||
placeholders => ?AUTHN_PLACEHOLDERS}).
|
||||
Template,
|
||||
#{
|
||||
replace_with => ReplaceWith,
|
||||
placeholders => ?AUTHN_PLACEHOLDERS
|
||||
}
|
||||
).
|
||||
|
||||
render_deep(Template, Credential) ->
|
||||
emqx_placeholder:proc_tmpl_deep(
|
||||
Template,
|
||||
Credential,
|
||||
#{return => full_binary, var_trans => fun handle_var/2}).
|
||||
Template,
|
||||
Credential,
|
||||
#{return => full_binary, var_trans => fun handle_var/2}
|
||||
).
|
||||
|
||||
render_str(Template, Credential) ->
|
||||
emqx_placeholder:proc_tmpl(
|
||||
Template,
|
||||
Credential,
|
||||
#{return => full_binary, var_trans => fun handle_var/2}).
|
||||
Template,
|
||||
Credential,
|
||||
#{return => full_binary, var_trans => fun handle_var/2}
|
||||
).
|
||||
|
||||
render_sql_params(ParamList, Credential) ->
|
||||
emqx_placeholder:proc_tmpl(
|
||||
ParamList,
|
||||
Credential,
|
||||
#{return => rawlist, var_trans => fun handle_sql_var/2}).
|
||||
ParamList,
|
||||
Credential,
|
||||
#{return => rawlist, var_trans => fun handle_sql_var/2}
|
||||
).
|
||||
|
||||
is_superuser(#{<<"is_superuser">> := <<"">>}) ->
|
||||
#{is_superuser => false};
|
||||
|
@ -114,8 +123,9 @@ bin(X) -> X.
|
|||
|
||||
cleanup_resources() ->
|
||||
lists:foreach(
|
||||
fun emqx_resource:remove_local/1,
|
||||
emqx_resource:list_group_instances(?RESOURCE_GROUP)).
|
||||
fun emqx_resource:remove_local/1,
|
||||
emqx_resource:list_group_instances(?RESOURCE_GROUP)
|
||||
).
|
||||
|
||||
make_resource_id(Name) ->
|
||||
NameBin = bin(Name),
|
||||
|
|
|
@ -26,12 +26,12 @@
|
|||
|
||||
-define(TCP_DEFAULT, 'tcp:default').
|
||||
|
||||
-define(
|
||||
assertAuthenticatorsMatch(Guard, Path),
|
||||
-define(assertAuthenticatorsMatch(Guard, Path),
|
||||
(fun() ->
|
||||
{ok, 200, Response} = request(get, uri(Path)),
|
||||
?assertMatch(Guard, jiffy:decode(Response, [return_maps]))
|
||||
end)()).
|
||||
end)()
|
||||
).
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
@ -42,12 +42,14 @@ groups() ->
|
|||
init_per_testcase(_, Config) ->
|
||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[?CONF_NS_ATOM],
|
||||
?GLOBAL),
|
||||
[?CONF_NS_ATOM],
|
||||
?GLOBAL
|
||||
),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[listeners, tcp, default, ?CONF_NS_ATOM],
|
||||
?TCP_DEFAULT),
|
||||
[listeners, tcp, default, ?CONF_NS_ATOM],
|
||||
?TCP_DEFAULT
|
||||
),
|
||||
|
||||
{atomic, ok} = mria:clear_table(emqx_authn_mnesia),
|
||||
Config.
|
||||
|
@ -55,8 +57,9 @@ init_per_testcase(_, Config) ->
|
|||
init_per_suite(Config) ->
|
||||
_ = application:load(emqx_conf),
|
||||
ok = emqx_common_test_helpers:start_apps(
|
||||
[emqx_authn, emqx_dashboard],
|
||||
fun set_special_configs/1),
|
||||
[emqx_authn, emqx_dashboard],
|
||||
fun set_special_configs/1
|
||||
),
|
||||
|
||||
?AUTHN:delete_chain(?GLOBAL),
|
||||
{ok, Chains} = ?AUTHN:list_chains(),
|
||||
|
@ -117,108 +120,132 @@ t_listener_authenticator_import_users(_) ->
|
|||
test_authenticator_import_users(["listeners", ?TCP_DEFAULT]).
|
||||
|
||||
test_authenticators(PathPrefix) ->
|
||||
|
||||
ValidConfig = emqx_authn_test_lib:http_example(),
|
||||
{ok, 200, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
ValidConfig),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
ValidConfig
|
||||
),
|
||||
|
||||
{ok, 409, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
ValidConfig),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
ValidConfig
|
||||
),
|
||||
|
||||
InvalidConfig0 = ValidConfig#{method => <<"delete">>},
|
||||
{ok, 400, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
InvalidConfig0),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
InvalidConfig0
|
||||
),
|
||||
|
||||
InvalidConfig1 = ValidConfig#{method => <<"get">>,
|
||||
headers => #{<<"content-type">> => <<"application/json">>}},
|
||||
InvalidConfig1 = ValidConfig#{
|
||||
method => <<"get">>,
|
||||
headers => #{<<"content-type">> => <<"application/json">>}
|
||||
},
|
||||
{ok, 400, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
InvalidConfig1),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
InvalidConfig1
|
||||
),
|
||||
|
||||
?assertAuthenticatorsMatch(
|
||||
[#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}],
|
||||
PathPrefix ++ [?CONF_NS]).
|
||||
[#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>}],
|
||||
PathPrefix ++ [?CONF_NS]
|
||||
).
|
||||
|
||||
test_authenticator(PathPrefix) ->
|
||||
ValidConfig0 = emqx_authn_test_lib:http_example(),
|
||||
{ok, 200, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
ValidConfig0),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
ValidConfig0
|
||||
),
|
||||
{ok, 200, _} = request(
|
||||
get,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])),
|
||||
get,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])
|
||||
),
|
||||
|
||||
{ok, 200, Res} = request(
|
||||
get,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])),
|
||||
get,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])
|
||||
),
|
||||
{ok, RList} = emqx_json:safe_decode(Res),
|
||||
Snd = fun ({_, Val}) -> Val end,
|
||||
Snd = fun({_, Val}) -> Val end,
|
||||
LookupVal = fun LookupV(List, RestJson) ->
|
||||
case List of
|
||||
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
|
||||
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
|
||||
end
|
||||
end,
|
||||
LookFun = fun (List) -> LookupVal(List, RList) end,
|
||||
MetricsList = [{<<"failed">>, 0},
|
||||
{<<"matched">>, 0},
|
||||
{<<"rate">>, 0.0},
|
||||
{<<"rate_last5m">>, 0.0},
|
||||
{<<"rate_max">>, 0.0},
|
||||
{<<"success">>, 0}],
|
||||
EqualFun = fun ({M, V}) ->
|
||||
?assertEqual(V, LookFun([<<"metrics">>,
|
||||
M]
|
||||
)
|
||||
) end,
|
||||
case List of
|
||||
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
|
||||
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
|
||||
end
|
||||
end,
|
||||
LookFun = fun(List) -> LookupVal(List, RList) end,
|
||||
MetricsList = [
|
||||
{<<"failed">>, 0},
|
||||
{<<"matched">>, 0},
|
||||
{<<"rate">>, 0.0},
|
||||
{<<"rate_last5m">>, 0.0},
|
||||
{<<"rate_max">>, 0.0},
|
||||
{<<"success">>, 0}
|
||||
],
|
||||
EqualFun = fun({M, V}) ->
|
||||
?assertEqual(
|
||||
V,
|
||||
LookFun([
|
||||
<<"metrics">>,
|
||||
M
|
||||
])
|
||||
)
|
||||
end,
|
||||
lists:map(EqualFun, MetricsList),
|
||||
?assertEqual(<<"connected">>,
|
||||
LookFun([<<"status">>
|
||||
])),
|
||||
?assertEqual(
|
||||
<<"connected">>,
|
||||
LookFun([<<"status">>])
|
||||
),
|
||||
{ok, 404, _} = request(
|
||||
get,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])),
|
||||
|
||||
get,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
|
||||
),
|
||||
|
||||
{ok, 404, _} = request(
|
||||
put,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database"]),
|
||||
emqx_authn_test_lib:built_in_database_example()),
|
||||
put,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database"]),
|
||||
emqx_authn_test_lib:built_in_database_example()
|
||||
),
|
||||
|
||||
InvalidConfig0 = ValidConfig0#{method => <<"delete">>},
|
||||
{ok, 400, _} = request(
|
||||
put,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||
InvalidConfig0),
|
||||
put,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||
InvalidConfig0
|
||||
),
|
||||
|
||||
InvalidConfig1 = ValidConfig0#{method => <<"get">>,
|
||||
headers => #{<<"content-type">> => <<"application/json">>}},
|
||||
InvalidConfig1 = ValidConfig0#{
|
||||
method => <<"get">>,
|
||||
headers => #{<<"content-type">> => <<"application/json">>}
|
||||
},
|
||||
{ok, 400, _} = request(
|
||||
put,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||
InvalidConfig1),
|
||||
put,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||
InvalidConfig1
|
||||
),
|
||||
|
||||
ValidConfig1 = ValidConfig0#{pool_size => 9},
|
||||
{ok, 200, _} = request(
|
||||
put,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||
ValidConfig1),
|
||||
put,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||
ValidConfig1
|
||||
),
|
||||
|
||||
{ok, 404, _} = request(
|
||||
delete,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])),
|
||||
delete,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
|
||||
),
|
||||
|
||||
{ok, 204, _} = request(
|
||||
delete,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])),
|
||||
delete,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])
|
||||
),
|
||||
|
||||
?assertAuthenticatorsMatch([], PathPrefix ++ [?CONF_NS]).
|
||||
|
||||
|
@ -226,64 +253,78 @@ test_authenticator_users(PathPrefix) ->
|
|||
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
|
||||
|
||||
{ok, 200, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
emqx_authn_test_lib:built_in_database_example()),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
emqx_authn_test_lib:built_in_database_example()
|
||||
),
|
||||
|
||||
InvalidUsers = [
|
||||
#{clientid => <<"u1">>, password => <<"p1">>},
|
||||
#{user_id => <<"u2">>},
|
||||
#{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}],
|
||||
#{clientid => <<"u1">>, password => <<"p1">>},
|
||||
#{user_id => <<"u2">>},
|
||||
#{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(User) -> {ok, 400, _} = request(post, UsersUri, User) end,
|
||||
InvalidUsers),
|
||||
|
||||
fun(User) -> {ok, 400, _} = request(post, UsersUri, User) end,
|
||||
InvalidUsers
|
||||
),
|
||||
|
||||
ValidUsers = [
|
||||
#{user_id => <<"u1">>, password => <<"p1">>},
|
||||
#{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true},
|
||||
#{user_id => <<"u3">>, password => <<"p3">>}],
|
||||
#{user_id => <<"u1">>, password => <<"p1">>},
|
||||
#{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true},
|
||||
#{user_id => <<"u3">>, password => <<"p3">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(User) ->
|
||||
{ok, 201, UserData} = request(post, UsersUri, User),
|
||||
CreatedUser = jiffy:decode(UserData, [return_maps]),
|
||||
?assertMatch(#{<<"user_id">> := _}, CreatedUser)
|
||||
end,
|
||||
ValidUsers),
|
||||
fun(User) ->
|
||||
{ok, 201, UserData} = request(post, UsersUri, User),
|
||||
CreatedUser = jiffy:decode(UserData, [return_maps]),
|
||||
?assertMatch(#{<<"user_id">> := _}, CreatedUser)
|
||||
end,
|
||||
ValidUsers
|
||||
),
|
||||
|
||||
{ok, 200, Page1Data} = request(get, UsersUri ++ "?page=1&limit=2"),
|
||||
|
||||
#{<<"data">> := Page1Users,
|
||||
<<"meta">> :=
|
||||
#{<<"page">> := 1,
|
||||
<<"limit">> := 2,
|
||||
<<"count">> := 3}} =
|
||||
jiffy:decode(Page1Data, [return_maps]),
|
||||
#{
|
||||
<<"data">> := Page1Users,
|
||||
<<"meta">> :=
|
||||
#{
|
||||
<<"page">> := 1,
|
||||
<<"limit">> := 2,
|
||||
<<"count">> := 3
|
||||
}
|
||||
} =
|
||||
jiffy:decode(Page1Data, [return_maps]),
|
||||
|
||||
{ok, 200, Page2Data} = request(get, UsersUri ++ "?page=2&limit=2"),
|
||||
|
||||
#{<<"data">> := Page2Users,
|
||||
<<"meta">> :=
|
||||
#{<<"page">> := 2,
|
||||
<<"limit">> := 2,
|
||||
<<"count">> := 3}} = jiffy:decode(Page2Data, [return_maps]),
|
||||
#{
|
||||
<<"data">> := Page2Users,
|
||||
<<"meta">> :=
|
||||
#{
|
||||
<<"page">> := 2,
|
||||
<<"limit">> := 2,
|
||||
<<"count">> := 3
|
||||
}
|
||||
} = jiffy:decode(Page2Data, [return_maps]),
|
||||
|
||||
?assertEqual(2, length(Page1Users)),
|
||||
?assertEqual(1, length(Page2Users)),
|
||||
|
||||
?assertEqual(
|
||||
[<<"u1">>, <<"u2">>, <<"u3">>],
|
||||
lists:usort([ UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])).
|
||||
[<<"u1">>, <<"u2">>, <<"u3">>],
|
||||
lists:usort([UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])
|
||||
).
|
||||
|
||||
test_authenticator_user(PathPrefix) ->
|
||||
UsersUri = uri(PathPrefix ++ [?CONF_NS, "password_based:built_in_database", "users"]),
|
||||
|
||||
{ok, 200, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
emqx_authn_test_lib:built_in_database_example()),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
emqx_authn_test_lib:built_in_database_example()
|
||||
),
|
||||
|
||||
User = #{user_id => <<"u1">>, password => <<"p1">>},
|
||||
{ok, 201, _} = request(post, UsersUri, User),
|
||||
|
@ -299,141 +340,161 @@ test_authenticator_user(PathPrefix) ->
|
|||
?assertNotMatch(#{<<"password">> := _}, FetchedUser),
|
||||
|
||||
ValidUserUpdates = [
|
||||
#{password => <<"p1">>},
|
||||
#{password => <<"p1">>, is_superuser => true}],
|
||||
#{password => <<"p1">>},
|
||||
#{password => <<"p1">>, is_superuser => true}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(UserUpdate) -> {ok, 200, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
|
||||
ValidUserUpdates),
|
||||
fun(UserUpdate) -> {ok, 200, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
|
||||
ValidUserUpdates
|
||||
),
|
||||
|
||||
InvalidUserUpdates = [#{user_id => <<"u1">>, password => <<"p1">>}],
|
||||
|
||||
lists:foreach(
|
||||
fun(UserUpdate) -> {ok, 400, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
|
||||
InvalidUserUpdates),
|
||||
fun(UserUpdate) -> {ok, 400, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
|
||||
InvalidUserUpdates
|
||||
),
|
||||
|
||||
{ok, 404, _} = request(delete, UsersUri ++ "/u123"),
|
||||
{ok, 204, _} = request(delete, UsersUri ++ "/u1").
|
||||
|
||||
test_authenticator_move(PathPrefix) ->
|
||||
AuthenticatorConfs = [
|
||||
emqx_authn_test_lib:http_example(),
|
||||
emqx_authn_test_lib:jwt_example(),
|
||||
emqx_authn_test_lib:built_in_database_example()
|
||||
],
|
||||
emqx_authn_test_lib:http_example(),
|
||||
emqx_authn_test_lib:jwt_example(),
|
||||
emqx_authn_test_lib:built_in_database_example()
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(Conf) ->
|
||||
{ok, 200, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
Conf)
|
||||
end,
|
||||
AuthenticatorConfs),
|
||||
fun(Conf) ->
|
||||
{ok, 200, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
Conf
|
||||
)
|
||||
end,
|
||||
AuthenticatorConfs
|
||||
),
|
||||
|
||||
?assertAuthenticatorsMatch(
|
||||
[
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"jwt">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]),
|
||||
[
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"jwt">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]
|
||||
),
|
||||
|
||||
%% Invalid moves
|
||||
|
||||
{ok, 400, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"up">>}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"up">>}
|
||||
),
|
||||
|
||||
{ok, 400, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{}
|
||||
),
|
||||
|
||||
{ok, 404, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"before:invalid">>}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"before:invalid">>}
|
||||
),
|
||||
|
||||
{ok, 404, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"before:password_based:redis">>}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"before:password_based:redis">>}
|
||||
),
|
||||
|
||||
{ok, 404, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"before:password_based:redis">>}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"before:password_based:redis">>}
|
||||
),
|
||||
|
||||
%% Valid moves
|
||||
|
||||
%% test front
|
||||
{ok, 204, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"front">>}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"front">>}
|
||||
),
|
||||
|
||||
?assertAuthenticatorsMatch(
|
||||
[
|
||||
#{<<"mechanism">> := <<"jwt">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]),
|
||||
[
|
||||
#{<<"mechanism">> := <<"jwt">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]
|
||||
),
|
||||
|
||||
%% test rear
|
||||
{ok, 204, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"rear">>}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"rear">>}
|
||||
),
|
||||
|
||||
?assertAuthenticatorsMatch(
|
||||
[
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
|
||||
#{<<"mechanism">> := <<"jwt">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]),
|
||||
[
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
|
||||
#{<<"mechanism">> := <<"jwt">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]
|
||||
),
|
||||
|
||||
%% test before
|
||||
{ok, 204, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"before:password_based:built_in_database">>}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
||||
#{position => <<"before:password_based:built_in_database">>}
|
||||
),
|
||||
|
||||
?assertAuthenticatorsMatch(
|
||||
[
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"jwt">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]),
|
||||
[
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"jwt">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]
|
||||
),
|
||||
|
||||
%% test after
|
||||
{ok, 204, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based%3Abuilt_in_database", "move"]),
|
||||
#{position => <<"after:password_based:http">>}),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS, "password_based%3Abuilt_in_database", "move"]),
|
||||
#{position => <<"after:password_based:http">>}
|
||||
),
|
||||
|
||||
?assertAuthenticatorsMatch(
|
||||
[
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
|
||||
#{<<"mechanism">> := <<"jwt">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]).
|
||||
[
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"http">>},
|
||||
#{<<"mechanism">> := <<"password_based">>, <<"backend">> := <<"built_in_database">>},
|
||||
#{<<"mechanism">> := <<"jwt">>}
|
||||
],
|
||||
PathPrefix ++ [?CONF_NS]
|
||||
).
|
||||
|
||||
test_authenticator_import_users(PathPrefix) ->
|
||||
ImportUri = uri(
|
||||
PathPrefix ++
|
||||
[?CONF_NS, "password_based:built_in_database", "import_users"]),
|
||||
|
||||
PathPrefix ++
|
||||
[?CONF_NS, "password_based:built_in_database", "import_users"]
|
||||
),
|
||||
|
||||
{ok, 200, _} = request(
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
emqx_authn_test_lib:built_in_database_example()),
|
||||
post,
|
||||
uri(PathPrefix ++ [?CONF_NS]),
|
||||
emqx_authn_test_lib:built_in_database_example()
|
||||
),
|
||||
|
||||
{ok, 400, _} = request(post, ImportUri, #{}),
|
||||
|
||||
|
|
|
@ -28,12 +28,12 @@
|
|||
|
||||
-define(HTTP_PORT, 33333).
|
||||
-define(HTTP_PATH, "/auth").
|
||||
-define(CREDENTIALS, #{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}).
|
||||
|
||||
-define(CREDENTIALS, #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}).
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
@ -46,8 +46,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
emqx_common_test_helpers:stop_apps([emqx_authn]),
|
||||
application:stop(cowboy),
|
||||
ok.
|
||||
|
@ -55,8 +56,9 @@ end_per_suite(_) ->
|
|||
init_per_testcase(_Case, Config) ->
|
||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
{ok, _} = emqx_authn_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH),
|
||||
Config.
|
||||
|
||||
|
@ -71,8 +73,9 @@ t_create(_Config) ->
|
|||
AuthConfig = raw_http_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_http}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
||||
|
||||
|
@ -81,83 +84,96 @@ t_create_invalid(_Config) ->
|
|||
|
||||
InvalidConfigs =
|
||||
[
|
||||
AuthConfig#{headers => []},
|
||||
AuthConfig#{method => delete}
|
||||
AuthConfig#{headers => []},
|
||||
AuthConfig#{method => delete}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(Config) ->
|
||||
ct:pal("creating authenticator with invalid config: ~p", [Config]),
|
||||
{error, _} =
|
||||
try
|
||||
emqx:update_config(
|
||||
fun(Config) ->
|
||||
ct:pal("creating authenticator with invalid config: ~p", [Config]),
|
||||
{error, _} =
|
||||
try
|
||||
emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config})
|
||||
catch
|
||||
throw:Error ->
|
||||
{error, Error}
|
||||
end,
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs).
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
)
|
||||
catch
|
||||
throw:Error ->
|
||||
{error, Error}
|
||||
end,
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs
|
||||
).
|
||||
|
||||
t_authenticate(_Config) ->
|
||||
ok = lists:foreach(
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
samples()).
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
samples()
|
||||
).
|
||||
|
||||
test_user_auth(#{handler := Handler,
|
||||
config_params := SpecificConfgParams,
|
||||
result := Result}) ->
|
||||
test_user_auth(#{
|
||||
handler := Handler,
|
||||
config_params := SpecificConfgParams,
|
||||
result := Result
|
||||
}) ->
|
||||
AuthConfig = maps:merge(raw_http_auth_config(), SpecificConfgParams),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
ok = emqx_authn_http_test_server:set_handler(Handler),
|
||||
|
||||
?assertEqual(Result, emqx_access_control:authenticate(?CREDENTIALS)),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL).
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
).
|
||||
|
||||
t_destroy(_Config) ->
|
||||
AuthConfig = raw_http_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
ok = emqx_authn_http_test_server:set_handler(
|
||||
fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(200, Req0),
|
||||
{ok, Req, State}
|
||||
end),
|
||||
fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(200, Req0),
|
||||
{ok, Req, State}
|
||||
end
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_http, state := State}]}
|
||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
||||
{ok, [#{provider := emqx_authn_http, state := State}]} =
|
||||
emqx_authentication:list_authenticators(?GLOBAL),
|
||||
|
||||
Credentials = maps:with([username, password], ?CREDENTIALS),
|
||||
|
||||
{ok, _} = emqx_authn_http:authenticate(
|
||||
Credentials,
|
||||
State),
|
||||
Credentials,
|
||||
State
|
||||
),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
|
||||
% Authenticator should not be usable anymore
|
||||
?assertMatch(
|
||||
ignore,
|
||||
emqx_authn_http:authenticate(
|
||||
Credentials,
|
||||
State)).
|
||||
ignore,
|
||||
emqx_authn_http:authenticate(
|
||||
Credentials,
|
||||
State
|
||||
)
|
||||
).
|
||||
|
||||
t_update(_Config) ->
|
||||
CorrectConfig = raw_http_auth_config(),
|
||||
|
@ -165,74 +181,80 @@ t_update(_Config) ->
|
|||
CorrectConfig#{url => <<"http://127.0.0.1:33333/invalid">>},
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||
),
|
||||
|
||||
ok = emqx_authn_http_test_server:set_handler(
|
||||
fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(200, Req0),
|
||||
{ok, Req, State}
|
||||
end),
|
||||
fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(200, Req0),
|
||||
{ok, Req, State}
|
||||
end
|
||||
),
|
||||
|
||||
{error, not_authorized} = emqx_access_control:authenticate(?CREDENTIALS),
|
||||
|
||||
% We update with config with correct query, provider should update and work properly
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:http">>, CorrectConfig}),
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:http">>, CorrectConfig}
|
||||
),
|
||||
|
||||
{ok,_} = emqx_access_control:authenticate(?CREDENTIALS).
|
||||
{ok, _} = emqx_access_control:authenticate(?CREDENTIALS).
|
||||
|
||||
t_is_superuser(_Config) ->
|
||||
Config = raw_http_auth_config(),
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
|
||||
Checks = [
|
||||
{json, <<"0">>, false},
|
||||
{json, <<"">>, false},
|
||||
{json, null, false},
|
||||
{json, 0, false},
|
||||
{json, <<"0">>, false},
|
||||
{json, <<"">>, false},
|
||||
{json, null, false},
|
||||
{json, 0, false},
|
||||
|
||||
{json, <<"1">>, true},
|
||||
{json, <<"val">>, true},
|
||||
{json, 1, true},
|
||||
{json, 123, true},
|
||||
{json, <<"1">>, true},
|
||||
{json, <<"val">>, true},
|
||||
{json, 1, true},
|
||||
{json, 123, true},
|
||||
|
||||
{form, <<"0">>, false},
|
||||
{form, <<"">>, false},
|
||||
{form, <<"0">>, false},
|
||||
{form, <<"">>, false},
|
||||
|
||||
{form, <<"1">>, true},
|
||||
{form, <<"val">>, true}
|
||||
],
|
||||
{form, <<"1">>, true},
|
||||
{form, <<"val">>, true}
|
||||
],
|
||||
|
||||
lists:foreach(fun test_is_superuser/1, Checks).
|
||||
|
||||
test_is_superuser({Kind, Value, ExpectedValue}) ->
|
||||
|
||||
{ContentType, Res} = case Kind of
|
||||
json ->
|
||||
{<<"application/json">>,
|
||||
jiffy:encode(#{is_superuser => Value})};
|
||||
form ->
|
||||
{<<"application/x-www-form-urlencoded">>,
|
||||
iolist_to_binary([<<"is_superuser=">>, Value])}
|
||||
end,
|
||||
{ContentType, Res} =
|
||||
case Kind of
|
||||
json ->
|
||||
{<<"application/json">>, jiffy:encode(#{is_superuser => Value})};
|
||||
form ->
|
||||
{<<"application/x-www-form-urlencoded">>,
|
||||
iolist_to_binary([<<"is_superuser=">>, Value])}
|
||||
end,
|
||||
|
||||
ok = emqx_authn_http_test_server:set_handler(
|
||||
fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{<<"content-type">> => ContentType},
|
||||
Res,
|
||||
Req0),
|
||||
{ok, Req, State}
|
||||
end),
|
||||
fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{<<"content-type">> => ContentType},
|
||||
Res,
|
||||
Req0
|
||||
),
|
||||
{ok, Req, State}
|
||||
end
|
||||
),
|
||||
|
||||
?assertMatch(
|
||||
{ok, #{is_superuser := ExpectedValue}},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
||||
{ok, #{is_superuser := ExpectedValue}},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -252,138 +274,159 @@ raw_http_auth_config() ->
|
|||
|
||||
samples() ->
|
||||
[
|
||||
%% simple get request
|
||||
#{handler => fun(Req0, State) ->
|
||||
#{username := <<"plain">>,
|
||||
password := <<"plain">>
|
||||
} = cowboy_req:match_qs([username, password], Req0),
|
||||
%% simple get request
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
#{
|
||||
username := <<"plain">>,
|
||||
password := <<"plain">>
|
||||
} = cowboy_req:match_qs([username, password], Req0),
|
||||
|
||||
Req = cowboy_req:reply(200, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
Req = cowboy_req:reply(200, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
|
||||
%% get request with json body response
|
||||
#{handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{<<"content-type">> => <<"application/json">>},
|
||||
jiffy:encode(#{is_superuser => true}),
|
||||
Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => true, user_property => #{}}}
|
||||
},
|
||||
%% get request with json body response
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{<<"content-type">> => <<"application/json">>},
|
||||
jiffy:encode(#{is_superuser => true}),
|
||||
Req0
|
||||
),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => true, user_property => #{}}}
|
||||
},
|
||||
|
||||
%% get request with url-form-encoded body response
|
||||
#{handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{<<"content-type">> =>
|
||||
<<"application/x-www-form-urlencoded">>},
|
||||
<<"is_superuser=true">>,
|
||||
Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => true, user_property => #{}}}
|
||||
},
|
||||
%% get request with url-form-encoded body response
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{
|
||||
<<"content-type">> =>
|
||||
<<"application/x-www-form-urlencoded">>
|
||||
},
|
||||
<<"is_superuser=true">>,
|
||||
Req0
|
||||
),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => true, user_property => #{}}}
|
||||
},
|
||||
|
||||
%% get request with response of unknown encoding
|
||||
#{handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{<<"content-type">> =>
|
||||
<<"test/plain">>},
|
||||
<<"is_superuser=true">>,
|
||||
Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
%% get request with response of unknown encoding
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{
|
||||
<<"content-type">> =>
|
||||
<<"test/plain">>
|
||||
},
|
||||
<<"is_superuser=true">>,
|
||||
Req0
|
||||
),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
|
||||
%% simple post request, application/json
|
||||
#{handler => fun(Req0, State) ->
|
||||
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
|
||||
#{<<"username">> := <<"plain">>,
|
||||
<<"password">> := <<"plain">>
|
||||
} = jiffy:decode(RawBody, [return_maps]),
|
||||
Req = cowboy_req:reply(200, Req1),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{
|
||||
method => post,
|
||||
headers => #{<<"content-type">> => <<"application/json">>}
|
||||
},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
%% simple post request, application/json
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
|
||||
#{
|
||||
<<"username">> := <<"plain">>,
|
||||
<<"password">> := <<"plain">>
|
||||
} = jiffy:decode(RawBody, [return_maps]),
|
||||
Req = cowboy_req:reply(200, Req1),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{
|
||||
method => post,
|
||||
headers => #{<<"content-type">> => <<"application/json">>}
|
||||
},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
|
||||
%% simple post request, application/x-www-form-urlencoded
|
||||
#{handler => fun(Req0, State) ->
|
||||
{ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0),
|
||||
#{<<"username">> := <<"plain">>,
|
||||
<<"password">> := <<"plain">>
|
||||
} = maps:from_list(PostVars),
|
||||
Req = cowboy_req:reply(200, Req1),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{
|
||||
method => post,
|
||||
headers => #{<<"content-type">> =>
|
||||
<<"application/x-www-form-urlencoded">>}
|
||||
},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
}
|
||||
%% simple post request, application/x-www-form-urlencoded
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
{ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0),
|
||||
#{
|
||||
<<"username">> := <<"plain">>,
|
||||
<<"password">> := <<"plain">>
|
||||
} = maps:from_list(PostVars),
|
||||
Req = cowboy_req:reply(200, Req1),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{
|
||||
method => post,
|
||||
headers => #{
|
||||
<<"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
|
||||
#{handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(204, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
%% custom headers
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
<<"Test Value">> = cowboy_req:header(<<"x-test-header">>, Req0),
|
||||
Req = cowboy_req:reply(200, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
|
||||
%% custom headers
|
||||
#{handler => fun(Req0, State) ->
|
||||
<<"Test Value">> = cowboy_req:header(<<"x-test-header">>, Req0),
|
||||
Req = cowboy_req:reply(200, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
%% 400 code
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(400, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {error, not_authorized}
|
||||
},
|
||||
|
||||
%% 400 code
|
||||
#{handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(400, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {error,not_authorized}
|
||||
},
|
||||
%% 500 code
|
||||
#{
|
||||
handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(500, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {error, not_authorized}
|
||||
},
|
||||
|
||||
%% 500 code
|
||||
#{handler => fun(Req0, State) ->
|
||||
Req = cowboy_req:reply(500, Req0),
|
||||
{ok, Req, State}
|
||||
end,
|
||||
config_params => #{},
|
||||
result => {error,not_authorized}
|
||||
},
|
||||
|
||||
%% Handling error
|
||||
#{handler => fun(Req0, State) ->
|
||||
error(woops),
|
||||
{ok, Req0, State}
|
||||
end,
|
||||
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) ->
|
||||
|
|
|
@ -26,11 +26,12 @@
|
|||
-export([init/1]).
|
||||
|
||||
% API
|
||||
-export([start_link/2,
|
||||
start_link/3,
|
||||
stop/0,
|
||||
set_handler/1
|
||||
]).
|
||||
-export([
|
||||
start_link/2,
|
||||
start_link/3,
|
||||
stop/0,
|
||||
set_handler/1
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% API
|
||||
|
@ -55,10 +56,11 @@ set_handler(F) when is_function(F, 2) ->
|
|||
|
||||
init([Port, Path, SSLOpts]) ->
|
||||
Dispatch = cowboy_router:compile(
|
||||
[
|
||||
{'_', [{Path, ?MODULE, []}]}
|
||||
]),
|
||||
|
||||
[
|
||||
{'_', [{Path, ?MODULE, []}]}
|
||||
]
|
||||
),
|
||||
|
||||
ProtoOpts = #{env => #{dispatch => Dispatch}},
|
||||
|
||||
Tab = ets:new(?MODULE, [set, named_table, public]),
|
||||
|
@ -83,23 +85,28 @@ init(Req, State) ->
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
transport_settings(Port, false) ->
|
||||
TransOpts = #{socket_opts => [{port, Port}],
|
||||
connection_type => supervisor},
|
||||
TransOpts = #{
|
||||
socket_opts => [{port, Port}],
|
||||
connection_type => supervisor
|
||||
},
|
||||
{ranch_tcp, TransOpts, cowboy_clear};
|
||||
|
||||
transport_settings(Port, SSLOpts) ->
|
||||
TransOpts = #{socket_opts => [{port, Port},
|
||||
{next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
|
||||
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|
||||
| SSLOpts],
|
||||
connection_type => supervisor},
|
||||
TransOpts = #{
|
||||
socket_opts => [
|
||||
{port, Port},
|
||||
{next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
|
||||
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|
||||
| SSLOpts
|
||||
],
|
||||
connection_type => supervisor
|
||||
},
|
||||
{ranch_ssl, TransOpts, cowboy_tls}.
|
||||
|
||||
default_handler(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
400,
|
||||
#{<<"content-type">> => <<"text/plain">>},
|
||||
<<"">>,
|
||||
Req0),
|
||||
400,
|
||||
#{<<"content-type">> => <<"text/plain">>},
|
||||
<<"">>,
|
||||
Req0
|
||||
),
|
||||
{ok, Req, State}.
|
||||
|
||||
|
|
|
@ -28,12 +28,12 @@
|
|||
|
||||
-define(HTTPS_PORT, 33333).
|
||||
-define(HTTPS_PATH, "/auth").
|
||||
-define(CREDENTIALS, #{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}).
|
||||
|
||||
-define(CREDENTIALS, #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}).
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
@ -46,8 +46,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
emqx_common_test_helpers:stop_apps([emqx_authn]),
|
||||
application:stop(cowboy),
|
||||
ok.
|
||||
|
@ -55,8 +56,9 @@ end_per_suite(_) ->
|
|||
init_per_testcase(_Case, Config) ->
|
||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
{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),
|
||||
Config.
|
||||
|
@ -70,46 +72,62 @@ end_per_testcase(_Case, _Config) ->
|
|||
|
||||
t_create(_Config) ->
|
||||
{ok, _} = create_https_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}),
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||
}
|
||||
),
|
||||
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
||||
{ok, _},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)
|
||||
).
|
||||
|
||||
t_create_invalid_domain(_Config) ->
|
||||
{ok, _} = create_https_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}),
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||
}
|
||||
),
|
||||
|
||||
?assertEqual(
|
||||
{error, not_authorized},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
||||
{error, not_authorized},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)
|
||||
).
|
||||
|
||||
t_create_invalid_version(_Config) ->
|
||||
{ok, _} = create_https_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>]}),
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>]
|
||||
}
|
||||
),
|
||||
|
||||
?assertEqual(
|
||||
{error, not_authorized},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
||||
{error, not_authorized},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)
|
||||
).
|
||||
|
||||
t_create_invalid_ciphers(_Config) ->
|
||||
{ok, _} = create_https_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES256-SHA384">>]}),
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES256-SHA384">>]
|
||||
}
|
||||
),
|
||||
|
||||
?assertEqual(
|
||||
{error, not_authorized},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)).
|
||||
{error, not_authorized},
|
||||
emqx_access_control:authenticate(?CREDENTIALS)
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -121,8 +139,9 @@ create_https_auth_with_ssl_opts(SpecificSSLOpts) ->
|
|||
|
||||
raw_https_auth_config(SpecificSSLOpts) ->
|
||||
SSLOpts = maps:merge(
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}),
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}
|
||||
),
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
enable => <<"true">>,
|
||||
|
@ -133,7 +152,7 @@ raw_https_auth_config(SpecificSSLOpts) ->
|
|||
body => #{<<"username">> => ?PH_USERNAME, <<"password">> => ?PH_PASSWORD},
|
||||
headers => #{<<"X-Test-Header">> => <<"Test Value">>},
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
}.
|
||||
|
||||
start_apps(Apps) ->
|
||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||
|
@ -147,15 +166,17 @@ cert_path(FileName) ->
|
|||
|
||||
cowboy_handler(Req0, State) ->
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
Req0),
|
||||
200,
|
||||
Req0
|
||||
),
|
||||
{ok, Req, State}.
|
||||
|
||||
server_ssl_opts() ->
|
||||
[{keyfile, cert_path("server.key")},
|
||||
{certfile, cert_path("server.crt")},
|
||||
{cacertfile, cert_path("ca.crt")},
|
||||
{verify, verify_none},
|
||||
{versions, ['tlsv1.2', 'tlsv1.3']},
|
||||
{ciphers, ["ECDHE-RSA-AES256-GCM-SHA384", "TLS_CHACHA20_POLY1305_SHA256"]}
|
||||
[
|
||||
{keyfile, cert_path("server.key")},
|
||||
{certfile, cert_path("server.crt")},
|
||||
{cacertfile, cert_path("ca.crt")},
|
||||
{verify, verify_none},
|
||||
{versions, ['tlsv1.2', 'tlsv1.3']},
|
||||
{ciphers, ["ECDHE-RSA-AES256-GCM-SHA384", "TLS_CHACHA20_POLY1305_SHA256"]}
|
||||
].
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
-define(JWKS_PORT, 33333).
|
||||
-define(JWKS_PATH, "/jwks.json").
|
||||
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
|
@ -51,24 +50,30 @@ end_per_suite(_) ->
|
|||
|
||||
t_jwt_authenticator_hmac_based(_) ->
|
||||
Secret = <<"abcdef">>,
|
||||
Config = #{mechanism => jwt,
|
||||
use_jwks => false,
|
||||
algorithm => 'hmac-based',
|
||||
secret => Secret,
|
||||
secret_base64_encoded => false,
|
||||
verify_claims => []},
|
||||
Config = #{
|
||||
mechanism => jwt,
|
||||
use_jwks => false,
|
||||
algorithm => 'hmac-based',
|
||||
secret => Secret,
|
||||
secret_base64_encoded => false,
|
||||
verify_claims => []
|
||||
},
|
||||
{ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config),
|
||||
|
||||
Payload = #{<<"username">> => <<"myuser">>},
|
||||
JWS = generate_jws('hmac-based', Payload, Secret),
|
||||
Credential = #{username => <<"myuser">>,
|
||||
password => JWS},
|
||||
Credential = #{
|
||||
username => <<"myuser">>,
|
||||
password => JWS
|
||||
},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State)),
|
||||
|
||||
Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true},
|
||||
JWS1 = generate_jws('hmac-based', Payload1, Secret),
|
||||
Credential1 = #{username => <<"myuser">>,
|
||||
password => JWS1},
|
||||
Credential1 = #{
|
||||
username => <<"myuser">>,
|
||||
password => JWS1
|
||||
},
|
||||
?assertEqual({ok, #{is_superuser => true}}, emqx_authn_jwt:authenticate(Credential1, State)),
|
||||
|
||||
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)),
|
||||
|
||||
%% secret_base64_encoded
|
||||
Config2 = Config#{secret => base64:encode(Secret),
|
||||
secret_base64_encoded => true},
|
||||
Config2 = Config#{
|
||||
secret => base64:encode(Secret),
|
||||
secret_base64_encoded => true
|
||||
},
|
||||
{ok, State2} = emqx_authn_jwt:update(Config2, State),
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential, State2)),
|
||||
|
||||
%% invalid secret
|
||||
BadConfig = Config#{secret => <<"emqxsecret">>,
|
||||
secret_base64_encoded => true},
|
||||
BadConfig = Config#{
|
||||
secret => <<"emqxsecret">>,
|
||||
secret_base64_encoded => true
|
||||
},
|
||||
{error, {invalid_parameter, secret}} = emqx_authn_jwt:create(?AUTHN_ID, BadConfig),
|
||||
|
||||
Config3 = Config#{verify_claims => [{<<"username">>, <<"${username}">>}]},
|
||||
{ok, State3} = emqx_authn_jwt:update(Config3, State2),
|
||||
?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
|
||||
Payload3 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"exp">> => erlang:system_time(second) - 60},
|
||||
Payload3 = #{
|
||||
<<"username">> => <<"myuser">>,
|
||||
<<"exp">> => erlang:system_time(second) - 60
|
||||
},
|
||||
JWS3 = generate_jws('hmac-based', Payload3, Secret),
|
||||
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">>
|
||||
, <<"exp">> => erlang:system_time(second) + 60},
|
||||
Payload4 = #{
|
||||
<<"username">> => <<"myuser">>,
|
||||
<<"exp">> => erlang:system_time(second) + 60
|
||||
},
|
||||
JWS4 = generate_jws('hmac-based', Payload4, Secret),
|
||||
Credential4 = Credential#{password => JWS4},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential4, State3)),
|
||||
|
||||
%% Issued At
|
||||
Payload5 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"iat">> => erlang:system_time(second) - 60},
|
||||
Payload5 = #{
|
||||
<<"username">> => <<"myuser">>,
|
||||
<<"iat">> => erlang:system_time(second) - 60
|
||||
},
|
||||
JWS5 = generate_jws('hmac-based', Payload5, Secret),
|
||||
Credential5 = Credential#{password => JWS5},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential5, State3)),
|
||||
|
||||
Payload6 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"iat">> => erlang:system_time(second) + 60},
|
||||
Payload6 = #{
|
||||
<<"username">> => <<"myuser">>,
|
||||
<<"iat">> => erlang:system_time(second) + 60
|
||||
},
|
||||
JWS6 = generate_jws('hmac-based', Payload6, Secret),
|
||||
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
|
||||
Payload7 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"nbf">> => erlang:system_time(second) - 60},
|
||||
Payload7 = #{
|
||||
<<"username">> => <<"myuser">>,
|
||||
<<"nbf">> => erlang:system_time(second) - 60
|
||||
},
|
||||
JWS7 = generate_jws('hmac-based', Payload7, Secret),
|
||||
Credential7 = Credential6#{password => JWS7},
|
||||
?assertEqual({ok, #{is_superuser => false}}, emqx_authn_jwt:authenticate(Credential7, State3)),
|
||||
|
||||
Payload8 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"nbf">> => erlang:system_time(second) + 60},
|
||||
Payload8 = #{
|
||||
<<"username">> => <<"myuser">>,
|
||||
<<"nbf">> => erlang:system_time(second) + 60
|
||||
},
|
||||
JWS8 = generate_jws('hmac-based', Payload8, Secret),
|
||||
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)),
|
||||
ok.
|
||||
|
@ -136,19 +166,25 @@ t_jwt_authenticator_hmac_based(_) ->
|
|||
t_jwt_authenticator_public_key(_) ->
|
||||
PublicKey = test_rsa_key(public),
|
||||
PrivateKey = test_rsa_key(private),
|
||||
Config = #{mechanism => jwt,
|
||||
use_jwks => false,
|
||||
algorithm => 'public-key',
|
||||
certificate => PublicKey,
|
||||
verify_claims => []},
|
||||
Config = #{
|
||||
mechanism => jwt,
|
||||
use_jwks => false,
|
||||
algorithm => 'public-key',
|
||||
certificate => PublicKey,
|
||||
verify_claims => []
|
||||
},
|
||||
{ok, State} = emqx_authn_jwt:create(?AUTHN_ID, Config),
|
||||
|
||||
Payload = #{<<"username">> => <<"myuser">>},
|
||||
JWS = generate_jws('public-key', Payload, PrivateKey),
|
||||
Credential = #{username => <<"myuser">>,
|
||||
password => JWS},
|
||||
Credential = #{
|
||||
username => <<"myuser">>,
|
||||
password => JWS
|
||||
},
|
||||
?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)),
|
||||
ok.
|
||||
|
@ -160,63 +196,77 @@ t_jwks_renewal(_Config) ->
|
|||
PrivateKey = test_rsa_key(private),
|
||||
Payload = #{<<"username">> => <<"myuser">>},
|
||||
JWS = generate_jws('public-key', Payload, PrivateKey),
|
||||
Credential = #{username => <<"myuser">>,
|
||||
password => JWS},
|
||||
|
||||
BadConfig0 = #{mechanism => jwt,
|
||||
algorithm => 'public-key',
|
||||
ssl => #{enable => false},
|
||||
verify_claims => [],
|
||||
Credential = #{
|
||||
username => <<"myuser">>,
|
||||
password => JWS
|
||||
},
|
||||
|
||||
use_jwks => true,
|
||||
endpoint => "https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT + 1) ++ ?JWKS_PATH,
|
||||
refresh_interval => 1000
|
||||
},
|
||||
BadConfig0 = #{
|
||||
mechanism => jwt,
|
||||
algorithm => 'public-key',
|
||||
ssl => #{enable => false},
|
||||
verify_claims => [],
|
||||
|
||||
use_jwks => true,
|
||||
endpoint => "https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT + 1) ++ ?JWKS_PATH,
|
||||
refresh_interval => 1000
|
||||
},
|
||||
|
||||
ok = snabbkaffe:start_trace(),
|
||||
|
||||
{{ok, State0}, _} = ?wait_async_action(
|
||||
emqx_authn_jwt:create(?AUTHN_ID, BadConfig0),
|
||||
#{?snk_kind := jwks_endpoint_response},
|
||||
10000),
|
||||
emqx_authn_jwt:create(?AUTHN_ID, BadConfig0),
|
||||
#{?snk_kind := jwks_endpoint_response},
|
||||
10000
|
||||
),
|
||||
|
||||
ok = snabbkaffe:stop(),
|
||||
|
||||
?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(),
|
||||
BadClientSSLOpts = ClientSSLOpts#{server_name_indication => "authn-server-unknown-host"},
|
||||
|
||||
BadConfig1 = BadConfig0#{endpoint =>
|
||||
"https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH,
|
||||
ssl => BadClientSSLOpts},
|
||||
BadConfig1 = BadConfig0#{
|
||||
endpoint =>
|
||||
"https://127.0.0.1:" ++ integer_to_list(?JWKS_PORT) ++ ?JWKS_PATH,
|
||||
ssl => BadClientSSLOpts
|
||||
},
|
||||
|
||||
ok = snabbkaffe:start_trace(),
|
||||
|
||||
{{ok, State1}, _} = ?wait_async_action(
|
||||
emqx_authn_jwt:create(?AUTHN_ID, BadConfig1),
|
||||
#{?snk_kind := jwks_endpoint_response},
|
||||
10000),
|
||||
emqx_authn_jwt:create(?AUTHN_ID, BadConfig1),
|
||||
#{?snk_kind := jwks_endpoint_response},
|
||||
10000
|
||||
),
|
||||
|
||||
ok = snabbkaffe:stop(),
|
||||
|
||||
?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},
|
||||
|
||||
ok = snabbkaffe:start_trace(),
|
||||
|
||||
{{ok, State2}, _} = ?wait_async_action(
|
||||
emqx_authn_jwt:update(GoodConfig, State1),
|
||||
#{?snk_kind := jwks_endpoint_response},
|
||||
10000),
|
||||
emqx_authn_jwt:update(GoodConfig, State1),
|
||||
#{?snk_kind := jwks_endpoint_response},
|
||||
10000
|
||||
),
|
||||
|
||||
ok = snabbkaffe:stop(),
|
||||
|
||||
?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)),
|
||||
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)),
|
||||
JWKS = jose_jwk_set:to_map([JWK], #{}),
|
||||
Req = cowboy_req:reply(
|
||||
200,
|
||||
#{<<"content-type">> => <<"application/json">>},
|
||||
jiffy:encode(JWKS),
|
||||
Req0),
|
||||
200,
|
||||
#{<<"content-type">> => <<"application/json">>},
|
||||
jiffy:encode(JWKS),
|
||||
Req0
|
||||
),
|
||||
{ok, Req, State}.
|
||||
|
||||
test_rsa_key(public) ->
|
||||
data_file("public_key.pem");
|
||||
|
||||
test_rsa_key(private) ->
|
||||
data_file("private_key.pem").
|
||||
|
||||
|
@ -250,32 +300,37 @@ cert_file(Name) ->
|
|||
|
||||
generate_jws('hmac-based', Payload, Secret) ->
|
||||
JWK = jose_jwk:from_oct(Secret),
|
||||
Header = #{ <<"alg">> => <<"HS256">>
|
||||
, <<"typ">> => <<"JWT">>
|
||||
},
|
||||
Header = #{
|
||||
<<"alg">> => <<"HS256">>,
|
||||
<<"typ">> => <<"JWT">>
|
||||
},
|
||||
Signed = jose_jwt:sign(JWK, Header, Payload),
|
||||
{_, JWS} = jose_jws:compact(Signed),
|
||||
JWS;
|
||||
generate_jws('public-key', Payload, PrivateKey) ->
|
||||
JWK = jose_jwk:from_pem_file(PrivateKey),
|
||||
Header = #{ <<"alg">> => <<"RS256">>
|
||||
, <<"typ">> => <<"JWT">>
|
||||
},
|
||||
Header = #{
|
||||
<<"alg">> => <<"RS256">>,
|
||||
<<"typ">> => <<"JWT">>
|
||||
},
|
||||
Signed = jose_jwt:sign(JWK, Header, Payload),
|
||||
{_, JWS} = jose_jws:compact(Signed),
|
||||
JWS.
|
||||
|
||||
client_ssl_opts() ->
|
||||
maps:merge(
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => true,
|
||||
verify => verify_peer,
|
||||
server_name_indication => "authn-server"
|
||||
}).
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{
|
||||
enable => true,
|
||||
verify => verify_peer,
|
||||
server_name_indication => "authn-server"
|
||||
}
|
||||
).
|
||||
|
||||
server_ssl_opts() ->
|
||||
[{keyfile, cert_file("server.key")},
|
||||
{certfile, cert_file("server.crt")},
|
||||
{cacertfile, cert_file("ca.crt")},
|
||||
{verify, verify_none}
|
||||
[
|
||||
{keyfile, cert_file("server.key")},
|
||||
{certfile, cert_file("server.crt")},
|
||||
{cacertfile, cert_file("ca.crt")},
|
||||
{verify, verify_none}
|
||||
].
|
||||
|
|
|
@ -76,7 +76,8 @@ t_check_schema(_Config) ->
|
|||
?assertException(
|
||||
throw,
|
||||
{emqx_authn_mnesia, _},
|
||||
hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigNotOk))).
|
||||
hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigNotOk))
|
||||
).
|
||||
|
||||
t_create(_) ->
|
||||
Config0 = config(),
|
||||
|
@ -110,7 +111,7 @@ t_destroy(_) ->
|
|||
ok = emqx_authn_mnesia:destroy(State0),
|
||||
|
||||
{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).
|
||||
|
||||
t_authenticate(_) ->
|
||||
|
@ -121,14 +122,17 @@ t_authenticate(_) ->
|
|||
{ok, _} = emqx_authn_mnesia:add_user(User, State),
|
||||
|
||||
{ok, _} = emqx_authn_mnesia:authenticate(
|
||||
#{username => <<"u">>, password => <<"p">>},
|
||||
State),
|
||||
#{username => <<"u">>, password => <<"p">>},
|
||||
State
|
||||
),
|
||||
{error, bad_username_or_password} = emqx_authn_mnesia:authenticate(
|
||||
#{username => <<"u">>, password => <<"badpass">>},
|
||||
State),
|
||||
#{username => <<"u">>, password => <<"badpass">>},
|
||||
State
|
||||
),
|
||||
ignore = emqx_authn_mnesia:authenticate(
|
||||
#{clientid => <<"u">>, password => <<"p">>},
|
||||
State).
|
||||
#{clientid => <<"u">>, password => <<"p">>},
|
||||
State
|
||||
).
|
||||
|
||||
t_add_user(_) ->
|
||||
Config = config(),
|
||||
|
@ -157,16 +161,19 @@ t_update_user(_) ->
|
|||
{ok, _} = emqx_authn_mnesia:add_user(User, State),
|
||||
|
||||
{error, not_found} = emqx_authn_mnesia:update_user(<<"u1">>, #{password => <<"p1">>}, State),
|
||||
{ok,
|
||||
#{user_id := <<"u">>,
|
||||
is_superuser := true}} = emqx_authn_mnesia:update_user(
|
||||
<<"u">>,
|
||||
#{password => <<"p1">>, is_superuser => true},
|
||||
State),
|
||||
{ok, #{
|
||||
user_id := <<"u">>,
|
||||
is_superuser := true
|
||||
}} = emqx_authn_mnesia:update_user(
|
||||
<<"u">>,
|
||||
#{password => <<"p1">>, is_superuser => true},
|
||||
State
|
||||
),
|
||||
|
||||
{ok, _} = emqx_authn_mnesia:authenticate(
|
||||
#{username => <<"u">>, password => <<"p1">>},
|
||||
State),
|
||||
#{username => <<"u">>, password => <<"p1">>},
|
||||
State
|
||||
),
|
||||
|
||||
{ok, #{is_superuser := true}} = emqx_authn_mnesia:lookup_user(<<"u">>, State).
|
||||
|
||||
|
@ -174,31 +181,47 @@ t_list_users(_) ->
|
|||
Config = config(),
|
||||
{ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
|
||||
|
||||
Users = [#{user_id => <<"u1">>, password => <<"p">>},
|
||||
#{user_id => <<"u2">>, password => <<"p">>},
|
||||
#{user_id => <<"u3">>, password => <<"p">>}],
|
||||
Users = [
|
||||
#{user_id => <<"u1">>, password => <<"p">>},
|
||||
#{user_id => <<"u2">>, password => <<"p">>},
|
||||
#{user_id => <<"u3">>, password => <<"p">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(U) -> {ok, _} = emqx_authn_mnesia:add_user(U, State) end,
|
||||
Users),
|
||||
fun(U) -> {ok, _} = emqx_authn_mnesia:add_user(U, State) end,
|
||||
Users
|
||||
),
|
||||
|
||||
#{data := [#{is_superuser := false,user_id := _},
|
||||
#{is_superuser := false,user_id := _}],
|
||||
meta := #{page := 1, limit := 2, count := 3}} = emqx_authn_mnesia:list_users(
|
||||
#{<<"page">> => 1, <<"limit">> => 2},
|
||||
State),
|
||||
#{
|
||||
data := [
|
||||
#{is_superuser := false, user_id := _},
|
||||
#{is_superuser := false, user_id := _}
|
||||
],
|
||||
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(
|
||||
#{<<"page">> => 2, <<"limit">> => 2},
|
||||
State),
|
||||
#{
|
||||
data := [#{is_superuser := false, user_id := _}],
|
||||
meta := #{page := 2, limit := 2, count := 3}
|
||||
} = 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(
|
||||
#{ <<"page">> => 1
|
||||
, <<"limit">> => 20
|
||||
, <<"like_username">> => <<"3">>},
|
||||
State).
|
||||
#{
|
||||
data := [#{is_superuser := false, user_id := <<"u3">>}],
|
||||
meta := #{page := 1, limit := 20, count := 1}
|
||||
} = emqx_authn_mnesia:list_users(
|
||||
#{
|
||||
<<"page">> => 1,
|
||||
<<"limit">> => 20,
|
||||
<<"like_username">> => <<"3">>
|
||||
},
|
||||
State
|
||||
).
|
||||
|
||||
t_import_users(_) ->
|
||||
Config0 = config(),
|
||||
|
@ -206,36 +229,44 @@ t_import_users(_) ->
|
|||
{ok, State} = emqx_authn_mnesia:create(?AUTHN_ID, Config),
|
||||
|
||||
ok = emqx_authn_mnesia:import_users(
|
||||
data_filename(<<"user-credentials.json">>),
|
||||
State),
|
||||
data_filename(<<"user-credentials.json">>),
|
||||
State
|
||||
),
|
||||
|
||||
ok = emqx_authn_mnesia:import_users(
|
||||
data_filename(<<"user-credentials.csv">>),
|
||||
State),
|
||||
data_filename(<<"user-credentials.csv">>),
|
||||
State
|
||||
),
|
||||
|
||||
{error, {unsupported_file_format, _}} = emqx_authn_mnesia:import_users(
|
||||
<<"/file/with/unknown.extension">>,
|
||||
State),
|
||||
<<"/file/with/unknown.extension">>,
|
||||
State
|
||||
),
|
||||
|
||||
{error, unknown_file_format} = emqx_authn_mnesia:import_users(
|
||||
<<"/file/with/no/extension">>,
|
||||
State),
|
||||
<<"/file/with/no/extension">>,
|
||||
State
|
||||
),
|
||||
|
||||
{error, enoent} = emqx_authn_mnesia:import_users(
|
||||
<<"/file/that/not/exist.json">>,
|
||||
State),
|
||||
<<"/file/that/not/exist.json">>,
|
||||
State
|
||||
),
|
||||
|
||||
{error, bad_format} = emqx_authn_mnesia:import_users(
|
||||
data_filename(<<"user-credentials-malformed-0.json">>),
|
||||
State),
|
||||
data_filename(<<"user-credentials-malformed-0.json">>),
|
||||
State
|
||||
),
|
||||
|
||||
{error, {_, invalid_json}} = emqx_authn_mnesia:import_users(
|
||||
data_filename(<<"user-credentials-malformed-1.json">>),
|
||||
State),
|
||||
data_filename(<<"user-credentials-malformed-1.json">>),
|
||||
State
|
||||
),
|
||||
|
||||
{error, bad_format} = emqx_authn_mnesia:import_users(
|
||||
data_filename(<<"user-credentials-malformed.csv">>),
|
||||
State).
|
||||
data_filename(<<"user-credentials-malformed.csv">>),
|
||||
State
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -246,7 +277,10 @@ data_filename(Name) ->
|
|||
filename:join([Dir, <<"data">>, Name]).
|
||||
|
||||
config() ->
|
||||
#{user_id_type => username,
|
||||
password_hash_algorithm => #{name => bcrypt,
|
||||
salt_rounds => 8}
|
||||
}.
|
||||
#{
|
||||
user_id_type => username,
|
||||
password_hash_algorithm => #{
|
||||
name => bcrypt,
|
||||
salt_rounds => 8
|
||||
}
|
||||
}.
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
|
||||
-define(MONGO_HOST, "mongo").
|
||||
-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),
|
||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
{ok, _} = mc_worker_api:connect(mongo_config()),
|
||||
Config.
|
||||
|
||||
|
@ -58,8 +58,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||
|
||||
|
@ -71,8 +72,9 @@ t_create(_Config) ->
|
|||
AuthConfig = raw_mongo_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_mongodb}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
||||
|
||||
|
@ -81,79 +83,93 @@ t_create_invalid(_Config) ->
|
|||
|
||||
InvalidConfigs =
|
||||
[
|
||||
AuthConfig#{mongo_type => <<"unknown">>},
|
||||
AuthConfig#{selector => <<"{ \"username\": \"${username}\" }">>},
|
||||
AuthConfig#{w_mode => <<"unknown">>}
|
||||
AuthConfig#{mongo_type => <<"unknown">>},
|
||||
AuthConfig#{selector => <<"{ \"username\": \"${username}\" }">>},
|
||||
AuthConfig#{w_mode => <<"unknown">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(Config) ->
|
||||
{error, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
fun(Config) ->
|
||||
{error, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs).
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs
|
||||
).
|
||||
|
||||
t_authenticate(_Config) ->
|
||||
ok = init_seeds(),
|
||||
ok = lists:foreach(
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
user_seeds()),
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
user_seeds()
|
||||
),
|
||||
ok = drop_seeds().
|
||||
|
||||
test_user_auth(#{credentials := Credentials0,
|
||||
config_params := SpecificConfigParams,
|
||||
result := Result}) ->
|
||||
test_user_auth(#{
|
||||
credentials := Credentials0,
|
||||
config_params := SpecificConfigParams,
|
||||
result := Result
|
||||
}) ->
|
||||
AuthConfig = maps:merge(raw_mongo_auth_config(), SpecificConfigParams),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
Credentials = Credentials0#{
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
},
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
},
|
||||
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL).
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
).
|
||||
|
||||
t_destroy(_Config) ->
|
||||
ok = init_seeds(),
|
||||
AuthConfig = raw_mongo_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_mongodb, state := State}]}
|
||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
||||
{ok, [#{provider := emqx_authn_mongodb, state := State}]} =
|
||||
emqx_authentication:list_authenticators(?GLOBAL),
|
||||
|
||||
{ok, _} = emqx_authn_mongodb:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State),
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State
|
||||
),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
|
||||
% Authenticator should not be usable anymore
|
||||
?assertMatch(
|
||||
ignore,
|
||||
emqx_authn_mongodb:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State)),
|
||||
ignore,
|
||||
emqx_authn_mongodb:authenticate(
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State
|
||||
)
|
||||
),
|
||||
|
||||
ok = drop_seeds().
|
||||
|
||||
|
@ -164,48 +180,55 @@ t_update(_Config) ->
|
|||
CorrectConfig#{selector => #{<<"wrongfield">> => <<"wrongvalue">>}},
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||
),
|
||||
|
||||
{error, not_authorized} = emqx_access_control:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}),
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}
|
||||
),
|
||||
|
||||
% We update with config with correct selector, provider should update and work properly
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:mongodb">>, CorrectConfig}),
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:mongodb">>, CorrectConfig}
|
||||
),
|
||||
|
||||
{ok,_} = emqx_access_control:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}),
|
||||
{ok, _} = emqx_access_control:authenticate(
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}
|
||||
),
|
||||
ok = drop_seeds().
|
||||
|
||||
t_is_superuser(_Config) ->
|
||||
Config = raw_mongo_auth_config(),
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
|
||||
Checks = [
|
||||
{<<"0">>, false},
|
||||
{<<"">>, false},
|
||||
{null, false},
|
||||
{false, false},
|
||||
{0, false},
|
||||
{<<"0">>, false},
|
||||
{<<"">>, false},
|
||||
{null, false},
|
||||
{false, false},
|
||||
{0, false},
|
||||
|
||||
{<<"1">>, true},
|
||||
{<<"val">>, true},
|
||||
{1, true},
|
||||
{123, true},
|
||||
{true, true}
|
||||
],
|
||||
{<<"1">>, true},
|
||||
{<<"val">>, true},
|
||||
{1, true},
|
||||
{123, true},
|
||||
{true, true}
|
||||
],
|
||||
|
||||
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">>, #{}),
|
||||
|
||||
UserData = #{
|
||||
username => <<"user">>,
|
||||
password_hash => <<"plainsalt">>,
|
||||
salt => <<"salt">>,
|
||||
is_superuser => Value
|
||||
},
|
||||
username => <<"user">>,
|
||||
password_hash => <<"plainsalt">>,
|
||||
salt => <<"salt">>,
|
||||
is_superuser => Value
|
||||
},
|
||||
|
||||
{{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, [UserData]),
|
||||
|
||||
Credentials = #{
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt,
|
||||
username => <<"user">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt,
|
||||
username => <<"user">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
|
||||
?assertEqual(
|
||||
{ok, #{is_superuser => ExpectedValue}},
|
||||
emqx_access_control:authenticate(Credentials)).
|
||||
{ok, #{is_superuser => ExpectedValue}},
|
||||
emqx_access_control:authenticate(Credentials)
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -238,146 +262,160 @@ test_is_superuser({Value, ExpectedValue}) ->
|
|||
|
||||
raw_mongo_auth_config() ->
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{name => <<"plain">>,
|
||||
salt_position => <<"suffix">>},
|
||||
enable => <<"true">>,
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"plain">>,
|
||||
salt_position => <<"suffix">>
|
||||
},
|
||||
enable => <<"true">>,
|
||||
|
||||
backend => <<"mongodb">>,
|
||||
mongo_type => <<"single">>,
|
||||
database => <<"mqtt">>,
|
||||
collection => <<"users">>,
|
||||
server => mongo_server(),
|
||||
w_mode => <<"unsafe">>,
|
||||
backend => <<"mongodb">>,
|
||||
mongo_type => <<"single">>,
|
||||
database => <<"mqtt">>,
|
||||
collection => <<"users">>,
|
||||
server => mongo_server(),
|
||||
w_mode => <<"unsafe">>,
|
||||
|
||||
selector => #{<<"username">> => <<"${username}">>},
|
||||
password_hash_field => <<"password_hash">>,
|
||||
salt_field => <<"salt">>,
|
||||
is_superuser_field => <<"is_superuser">>
|
||||
}.
|
||||
selector => #{<<"username">> => <<"${username}">>},
|
||||
password_hash_field => <<"password_hash">>,
|
||||
salt_field => <<"salt">>,
|
||||
is_superuser_field => <<"is_superuser">>
|
||||
}.
|
||||
|
||||
user_seeds() ->
|
||||
[#{data => #{
|
||||
username => <<"plain">>,
|
||||
password_hash => <<"plainsalt">>,
|
||||
salt => <<"salt">>,
|
||||
is_superuser => <<"1">>
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
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
|
||||
[
|
||||
#{
|
||||
data => #{
|
||||
username => <<"plain">>,
|
||||
password_hash => <<"plainsalt">>,
|
||||
salt => <<"salt">>,
|
||||
is_superuser => <<"1">>
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => true}}
|
||||
},
|
||||
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">>,
|
||||
password_hash =>
|
||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||
is_superuser => 0
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
#{
|
||||
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 => <<"bcrypt0">>,
|
||||
password_hash =>
|
||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||
is_superuser => <<"0">>
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
% clientid variable & username credentials
|
||||
selector => #{<<"username">> => <<"${clientid}">>},
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {error,not_authorized}
|
||||
},
|
||||
#{
|
||||
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 => <<"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 => <<"bcrypt">>,
|
||||
password_hash =>
|
||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||
is_superuser => 0
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
|
||||
#{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}
|
||||
}
|
||||
#{
|
||||
data => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password_hash =>
|
||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||
is_superuser => <<"0">>
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
% clientid variable & username credentials
|
||||
selector => #{<<"username">> => <<"${clientid}">>},
|
||||
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() ->
|
||||
|
@ -390,14 +428,14 @@ drop_seeds() ->
|
|||
ok.
|
||||
|
||||
mongo_server() ->
|
||||
iolist_to_binary(io_lib:format("~s",[?MONGO_HOST])).
|
||||
iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
|
||||
|
||||
mongo_config() ->
|
||||
[
|
||||
{database, <<"mqtt">>},
|
||||
{host, ?MONGO_HOST},
|
||||
{port, ?MONGO_DEFAULT_PORT},
|
||||
{register, ?MONGO_CLIENT}
|
||||
{database, <<"mqtt">>},
|
||||
{host, ?MONGO_HOST},
|
||||
{port, ?MONGO_DEFAULT_PORT},
|
||||
{register, ?MONGO_CLIENT}
|
||||
].
|
||||
|
||||
start_apps(Apps) ->
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
|
||||
-define(MONGO_HOST, "mongo-tls").
|
||||
|
||||
-define(PATH, [authentication]).
|
||||
|
@ -37,8 +36,9 @@ init_per_testcase(_TestCase, Config) ->
|
|||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
Config.
|
||||
|
||||
init_per_suite(Config) ->
|
||||
|
@ -54,8 +54,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||
|
||||
|
@ -72,69 +73,90 @@ end_per_suite(_Config) ->
|
|||
|
||||
t_create(_Config) ->
|
||||
?check_trace(
|
||||
create_mongo_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]}),
|
||||
fun({ok, _}, Trace) ->
|
||||
create_mongo_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||
}
|
||||
),
|
||||
fun({ok, _}, Trace) ->
|
||||
?assertMatch(
|
||||
[ok | _],
|
||||
?projection(
|
||||
status,
|
||||
?of_kind(emqx_connector_mongo_health_check, Trace)))
|
||||
end).
|
||||
|
||||
[ok | _],
|
||||
?projection(
|
||||
status,
|
||||
?of_kind(emqx_connector_mongo_health_check, Trace)
|
||||
)
|
||||
)
|
||||
end
|
||||
).
|
||||
|
||||
t_create_invalid_server_name(_Config) ->
|
||||
?check_trace(
|
||||
create_mongo_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>}),
|
||||
fun(_, Trace) ->
|
||||
?assertNotEqual(
|
||||
[ok],
|
||||
?projection(
|
||||
status,
|
||||
?of_kind(emqx_connector_mongo_health_check, Trace)))
|
||||
end).
|
||||
|
||||
create_mongo_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>
|
||||
}
|
||||
),
|
||||
fun(_, Trace) ->
|
||||
?assertNotEqual(
|
||||
[ok],
|
||||
?projection(
|
||||
status,
|
||||
?of_kind(emqx_connector_mongo_health_check, Trace)
|
||||
)
|
||||
)
|
||||
end
|
||||
).
|
||||
|
||||
%% docker-compose-mongo-single-tls.yaml:
|
||||
%% --tlsDisabledProtocols TLS1_0,TLS1_1
|
||||
|
||||
t_create_invalid_version(_Config) ->
|
||||
?check_trace(
|
||||
create_mongo_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>]}),
|
||||
fun(_, Trace) ->
|
||||
?assertNotEqual(
|
||||
[ok],
|
||||
?projection(
|
||||
status,
|
||||
?of_kind(emqx_connector_mongo_health_check, Trace)))
|
||||
end).
|
||||
|
||||
create_mongo_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>]
|
||||
}
|
||||
),
|
||||
fun(_, Trace) ->
|
||||
?assertNotEqual(
|
||||
[ok],
|
||||
?projection(
|
||||
status,
|
||||
?of_kind(emqx_connector_mongo_health_check, Trace)
|
||||
)
|
||||
)
|
||||
end
|
||||
).
|
||||
|
||||
%% docker-compose-mongo-single-tls.yaml:
|
||||
%% --setParameter opensslCipherConfig='HIGH:!EXPORT:!aNULL:!DHE:!kDHE@STRENGTH'
|
||||
|
||||
t_invalid_ciphers(_Config) ->
|
||||
?check_trace(
|
||||
create_mongo_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"DHE-RSA-AES256-GCM-SHA384">>]}),
|
||||
fun(_, Trace) ->
|
||||
?assertNotEqual(
|
||||
[ok],
|
||||
?projection(
|
||||
status,
|
||||
?of_kind(emqx_connector_mongo_health_check, Trace)))
|
||||
end).
|
||||
create_mongo_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"DHE-RSA-AES256-GCM-SHA384">>]
|
||||
}
|
||||
),
|
||||
fun(_, Trace) ->
|
||||
?assertNotEqual(
|
||||
[ok],
|
||||
?projection(
|
||||
status,
|
||||
?of_kind(emqx_connector_mongo_health_check, Trace)
|
||||
)
|
||||
)
|
||||
end
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -148,35 +170,38 @@ create_mongo_auth_with_ssl_opts(SpecificSSLOpts) ->
|
|||
|
||||
raw_mongo_auth_config(SpecificSSLOpts) ->
|
||||
SSLOpts = maps:merge(
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}),
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}
|
||||
),
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{name => <<"plain">>,
|
||||
salt_position => <<"suffix">>},
|
||||
enable => <<"true">>,
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"plain">>,
|
||||
salt_position => <<"suffix">>
|
||||
},
|
||||
enable => <<"true">>,
|
||||
|
||||
backend => <<"mongodb">>,
|
||||
pool_size => 2,
|
||||
mongo_type => <<"single">>,
|
||||
database => <<"mqtt">>,
|
||||
collection => <<"users">>,
|
||||
server => mongo_server(),
|
||||
w_mode => <<"unsafe">>,
|
||||
backend => <<"mongodb">>,
|
||||
pool_size => 2,
|
||||
mongo_type => <<"single">>,
|
||||
database => <<"mqtt">>,
|
||||
collection => <<"users">>,
|
||||
server => mongo_server(),
|
||||
w_mode => <<"unsafe">>,
|
||||
|
||||
selector => #{<<"username">> => <<"${username}">>},
|
||||
password_hash_field => <<"password_hash">>,
|
||||
salt_field => <<"salt">>,
|
||||
is_superuser_field => <<"is_superuser">>,
|
||||
topology => #{
|
||||
server_selection_timeout_ms => <<"10000ms">>
|
||||
},
|
||||
selector => #{<<"username">> => <<"${username}">>},
|
||||
password_hash_field => <<"password_hash">>,
|
||||
salt_field => <<"salt">>,
|
||||
is_superuser_field => <<"is_superuser">>,
|
||||
topology => #{
|
||||
server_selection_timeout_ms => <<"10000ms">>
|
||||
},
|
||||
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
|
||||
mongo_server() ->
|
||||
iolist_to_binary(io_lib:format("~s",[?MONGO_HOST])).
|
||||
iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
|
||||
|
||||
start_apps(Apps) ->
|
||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||
|
|
|
@ -21,28 +21,36 @@
|
|||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
%% API
|
||||
-export([start_link/2,
|
||||
stop/1]).
|
||||
-export([
|
||||
start_link/2,
|
||||
stop/1
|
||||
]).
|
||||
|
||||
-export([send/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
|
||||
-export([init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2]).
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2
|
||||
]).
|
||||
|
||||
-define(TIMEOUT, 1000).
|
||||
-define(TCP_OPTIONS, [binary, {packet, raw}, {active, once},
|
||||
{nodelay, true}]).
|
||||
-define(TCP_OPTIONS, [
|
||||
binary,
|
||||
{packet, raw},
|
||||
{active, once},
|
||||
{nodelay, true}
|
||||
]).
|
||||
|
||||
-define(PARSE_OPTIONS,
|
||||
#{strict_mode => false,
|
||||
max_size => ?MAX_PACKET_SIZE,
|
||||
version => ?MQTT_PROTO_V5
|
||||
}).
|
||||
-define(PARSE_OPTIONS, #{
|
||||
strict_mode => false,
|
||||
max_size => ?MAX_PACKET_SIZE,
|
||||
version => ?MQTT_PROTO_V5
|
||||
}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
|
@ -63,26 +71,30 @@ send(Pid, Packet) ->
|
|||
|
||||
init([Host, Port, Owner]) ->
|
||||
{ok, Socket} = gen_tcp:connect(Host, Port, ?TCP_OPTIONS, ?TIMEOUT),
|
||||
{ok, #{owner => Owner,
|
||||
socket => Socket,
|
||||
parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS)
|
||||
}}.
|
||||
{ok, #{
|
||||
owner => Owner,
|
||||
socket => Socket,
|
||||
parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS)
|
||||
}}.
|
||||
|
||||
handle_info({tcp, _Sock, Data}, #{parse_state := PSt,
|
||||
owner := Owner,
|
||||
socket := Socket} = St) ->
|
||||
handle_info(
|
||||
{tcp, _Sock, Data},
|
||||
#{
|
||||
parse_state := PSt,
|
||||
owner := Owner,
|
||||
socket := Socket
|
||||
} = St
|
||||
) ->
|
||||
{NewPSt, Packets} = process_incoming(PSt, Data, []),
|
||||
ok = deliver(Owner, Packets),
|
||||
ok = run_sock(Socket),
|
||||
{noreply, St#{parse_state => NewPSt}};
|
||||
|
||||
handle_info({tcp_closed, _Sock}, St) ->
|
||||
{stop, normal, St}.
|
||||
|
||||
handle_call({send, Packet}, _From, #{socket := Socket} = St) ->
|
||||
ok = gen_tcp:send(Socket, emqx_frame:serialize(Packet, ?MQTT_PROTO_V5)),
|
||||
{reply, ok, St};
|
||||
|
||||
handle_call(stop, _From, #{socket := Socket} = St) ->
|
||||
ok = gen_tcp:close(Socket),
|
||||
{stop, normal, ok, St}.
|
||||
|
@ -100,16 +112,16 @@ terminate(_Reason, _St) ->
|
|||
process_incoming(PSt, Data, Packets) ->
|
||||
case emqx_frame:parse(Data, PSt) of
|
||||
{more, NewPSt} ->
|
||||
{NewPSt, lists:reverse(Packets)};
|
||||
{NewPSt, lists:reverse(Packets)};
|
||||
{ok, Packet, Rest, NewPSt} ->
|
||||
process_incoming(NewPSt, Rest, [Packet | Packets])
|
||||
end.
|
||||
|
||||
deliver(_Owner, []) -> ok;
|
||||
deliver(_Owner, []) ->
|
||||
ok;
|
||||
deliver(Owner, [Packet | Packets]) ->
|
||||
Owner ! {packet, Packet},
|
||||
deliver(Owner, Packets).
|
||||
|
||||
|
||||
run_sock(Socket) ->
|
||||
inet:setopts(Socket, [{active, once}]).
|
||||
|
|
|
@ -40,8 +40,9 @@ init_per_testcase(_, Config) ->
|
|||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
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 = start_apps([emqx_resource, emqx_connector]),
|
||||
{ok, _} = emqx_resource:create_local(
|
||||
?MYSQL_RESOURCE,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_mysql,
|
||||
mysql_config(),
|
||||
#{}),
|
||||
?MYSQL_RESOURCE,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_mysql,
|
||||
mysql_config(),
|
||||
#{}
|
||||
),
|
||||
Config;
|
||||
false ->
|
||||
{skip, no_mysql}
|
||||
|
@ -71,8 +73,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
|
||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||
|
@ -85,8 +88,9 @@ t_create(_Config) ->
|
|||
AuthConfig = raw_mysql_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_mysql}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID).
|
||||
|
@ -96,108 +100,132 @@ t_create_invalid(_Config) ->
|
|||
|
||||
InvalidConfigs =
|
||||
[
|
||||
maps:without([server], AuthConfig),
|
||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||
AuthConfig#{password => <<"wrongpass">>},
|
||||
AuthConfig#{database => <<"wrongdatabase">>}
|
||||
maps:without([server], AuthConfig),
|
||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||
AuthConfig#{password => <<"wrongpass">>},
|
||||
AuthConfig#{database => <<"wrongdatabase">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(Config) ->
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
{ok, _} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs).
|
||||
fun(Config) ->
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
{ok, _} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs
|
||||
).
|
||||
|
||||
t_authenticate(_Config) ->
|
||||
ok = lists:foreach(
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
user_seeds()).
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
user_seeds()
|
||||
).
|
||||
|
||||
test_user_auth(#{credentials := Credentials0,
|
||||
config_params := SpecificConfigParams,
|
||||
result := Result}) ->
|
||||
test_user_auth(#{
|
||||
credentials := Credentials0,
|
||||
config_params := SpecificConfigParams,
|
||||
result := Result
|
||||
}) ->
|
||||
AuthConfig = maps:merge(raw_mysql_auth_config(), SpecificConfigParams),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
Credentials = Credentials0#{
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
},
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
},
|
||||
|
||||
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL).
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
).
|
||||
|
||||
t_destroy(_Config) ->
|
||||
AuthConfig = raw_mysql_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_mysql, state := State}]}
|
||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
||||
{ok, [#{provider := emqx_authn_mysql, state := State}]} =
|
||||
emqx_authentication:list_authenticators(?GLOBAL),
|
||||
|
||||
{ok, _} = emqx_authn_mysql:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State),
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State
|
||||
),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
|
||||
% Authenticator should not be usable anymore
|
||||
?assertMatch(
|
||||
ignore,
|
||||
emqx_authn_mysql:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State)).
|
||||
ignore,
|
||||
emqx_authn_mysql:authenticate(
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State
|
||||
)
|
||||
).
|
||||
|
||||
t_update(_Config) ->
|
||||
CorrectConfig = raw_mysql_auth_config(),
|
||||
IncorrectConfig =
|
||||
CorrectConfig#{
|
||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
||||
FROM wrong_table where username = ${username} LIMIT 1">>},
|
||||
query =>
|
||||
<<
|
||||
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||
" FROM wrong_table where username = ${username} LIMIT 1"
|
||||
>>
|
||||
},
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||
),
|
||||
|
||||
{error, not_authorized} = emqx_access_control:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}),
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}
|
||||
),
|
||||
|
||||
% We update with config with correct query, provider should update and work properly
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:mysql">>, CorrectConfig}),
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:mysql">>, CorrectConfig}
|
||||
),
|
||||
|
||||
{ok,_} = emqx_access_control:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}).
|
||||
{ok, _} = emqx_access_control:authenticate(
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -205,207 +233,248 @@ t_update(_Config) ->
|
|||
|
||||
raw_mysql_auth_config() ->
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{name => <<"plain">>,
|
||||
salt_position => <<"suffix">>},
|
||||
enable => <<"true">>,
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"plain">>,
|
||||
salt_position => <<"suffix">>
|
||||
},
|
||||
enable => <<"true">>,
|
||||
|
||||
backend => <<"mysql">>,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
backend => <<"mysql">>,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
|
||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
||||
FROM users where username = ${username} LIMIT 1">>,
|
||||
server => mysql_server()
|
||||
}.
|
||||
query =>
|
||||
<<
|
||||
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||
" FROM users where username = ${username} LIMIT 1"
|
||||
>>,
|
||||
server => mysql_server()
|
||||
}.
|
||||
|
||||
user_seeds() ->
|
||||
[#{data => #{
|
||||
username => "plain",
|
||||
password_hash => "plainsalt",
|
||||
salt => "salt",
|
||||
is_superuser_str => "1"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>},
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => true}}
|
||||
},
|
||||
|
||||
#{data => #{
|
||||
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
|
||||
[
|
||||
#{
|
||||
data => #{
|
||||
username => "plain",
|
||||
password_hash => "plainsalt",
|
||||
salt => "salt",
|
||||
is_superuser_str => "1"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => true}}
|
||||
},
|
||||
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">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||
is_superuser_int => 0
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||
FROM users where username = ${username} LIMIT 1">>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
#{
|
||||
data => #{
|
||||
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 => <<"bcrypt">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||
FROM users where username = ${username} LIMIT 1">>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
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\n"
|
||||
" FROM users where username = ${clientid} LIMIT 1"
|
||||
>>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"sha256">>,
|
||||
salt_position => <<"prefix">>
|
||||
}
|
||||
},
|
||||
result => {ok, #{is_superuser => true}}
|
||||
},
|
||||
|
||||
#{data => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||
is_superuser_str => "0"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
% clientid variable & username credentials
|
||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||
FROM users where username = ${clientid} LIMIT 1">>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {error,not_authorized}
|
||||
},
|
||||
#{
|
||||
data => #{
|
||||
username => <<"bcrypt">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||
is_superuser_int => 0
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
query =>
|
||||
<<
|
||||
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||
" FROM users where username = ${username} LIMIT 1"
|
||||
>>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
|
||||
#{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
|
||||
FROM users where username = ${username} LIMIT 1">>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {error,not_authorized}
|
||||
},
|
||||
#{
|
||||
data => #{
|
||||
username => <<"bcrypt">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
query =>
|
||||
<<
|
||||
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||
" FROM users where username = ${username} LIMIT 1"
|
||||
>>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
|
||||
#{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}
|
||||
}
|
||||
#{
|
||||
data => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||
is_superuser_str => "0"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
% clientid variable & username credentials
|
||||
query =>
|
||||
<<
|
||||
"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() ->
|
||||
ok = drop_seeds(),
|
||||
ok = q("CREATE TABLE users(
|
||||
username VARCHAR(255),
|
||||
password_hash VARCHAR(255),
|
||||
salt VARCHAR(255),
|
||||
is_superuser_str VARCHAR(255),
|
||||
is_superuser_int TINYINT)"),
|
||||
ok = q(
|
||||
"CREATE TABLE users(\n"
|
||||
" username VARCHAR(255),\n"
|
||||
" password_hash VARCHAR(255),\n"
|
||||
" salt VARCHAR(255),\n"
|
||||
" is_superuser_str VARCHAR(255),\n"
|
||||
" is_superuser_int TINYINT)"
|
||||
),
|
||||
|
||||
Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int],
|
||||
InsertQuery = "INSERT INTO users(username, password_hash, salt, "
|
||||
" is_superuser_str, is_superuser_int) VALUES(?, ?, ?, ?, ?)",
|
||||
InsertQuery =
|
||||
"INSERT INTO users(username, password_hash, salt, "
|
||||
" is_superuser_str, is_superuser_int) VALUES(?, ?, ?, ?, ?)",
|
||||
|
||||
lists:foreach(
|
||||
fun(#{data := Values}) ->
|
||||
Params = [maps:get(F, Values, null) || F <- Fields],
|
||||
ok = q(InsertQuery, Params)
|
||||
end,
|
||||
user_seeds()).
|
||||
fun(#{data := Values}) ->
|
||||
Params = [maps:get(F, Values, null) || F <- Fields],
|
||||
ok = q(InsertQuery, Params)
|
||||
end,
|
||||
user_seeds()
|
||||
).
|
||||
|
||||
q(Sql) ->
|
||||
emqx_resource:query(
|
||||
?MYSQL_RESOURCE,
|
||||
{sql, Sql}).
|
||||
?MYSQL_RESOURCE,
|
||||
{sql, Sql}
|
||||
).
|
||||
|
||||
q(Sql, Params) ->
|
||||
emqx_resource:query(
|
||||
?MYSQL_RESOURCE,
|
||||
{sql, Sql, Params}).
|
||||
?MYSQL_RESOURCE,
|
||||
{sql, Sql, Params}
|
||||
).
|
||||
|
||||
drop_seeds() ->
|
||||
ok = q("DROP TABLE IF EXISTS users").
|
||||
|
||||
mysql_server() ->
|
||||
iolist_to_binary(io_lib:format("~s",[?MYSQL_HOST])).
|
||||
iolist_to_binary(io_lib:format("~s", [?MYSQL_HOST])).
|
||||
|
||||
mysql_config() ->
|
||||
#{auto_reconnect => true,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
pool_size => 8,
|
||||
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
|
||||
ssl => #{enable => false}
|
||||
}.
|
||||
#{
|
||||
auto_reconnect => true,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
pool_size => 8,
|
||||
server => {?MYSQL_HOST, ?MYSQL_DEFAULT_PORT},
|
||||
ssl => #{enable => false}
|
||||
}.
|
||||
|
||||
start_apps(Apps) ->
|
||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||
|
|
|
@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
|
|||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
Config.
|
||||
|
||||
init_per_suite(Config) ->
|
||||
|
@ -56,8 +57,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||
|
||||
|
@ -70,38 +72,53 @@ t_create(_Config) ->
|
|||
%% -connect authn-server:3306 -starttls mysql \
|
||||
%% -cert client.crt -key client.key -CAfile ca.crt
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_mysql_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]})).
|
||||
{ok, _},
|
||||
create_mysql_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||
}
|
||||
)
|
||||
).
|
||||
|
||||
t_create_invalid(_Config) ->
|
||||
|
||||
%% invalid server_name
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_mysql_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>})),
|
||||
{ok, _},
|
||||
create_mysql_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>
|
||||
}
|
||||
)
|
||||
),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
%% incompatible versions
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_mysql_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>]})),
|
||||
{ok, _},
|
||||
create_mysql_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>]
|
||||
}
|
||||
)
|
||||
),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
%% incompatible ciphers
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_mysql_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]})).
|
||||
{ok, _},
|
||||
create_mysql_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]
|
||||
}
|
||||
)
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -113,27 +130,33 @@ create_mysql_auth_with_ssl_opts(SpecificSSLOpts) ->
|
|||
|
||||
raw_mysql_auth_config(SpecificSSLOpts) ->
|
||||
SSLOpts = maps:merge(
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}),
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}
|
||||
),
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{name => <<"plain">>,
|
||||
salt_position => <<"suffix">>},
|
||||
enable => <<"true">>,
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"plain">>,
|
||||
salt_position => <<"suffix">>
|
||||
},
|
||||
enable => <<"true">>,
|
||||
|
||||
backend => <<"mysql">>,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
backend => <<"mysql">>,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
|
||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
||||
FROM users where username = ${username} LIMIT 1">>,
|
||||
server => mysql_server(),
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
query =>
|
||||
<<
|
||||
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||
" FROM users where username = ${username} LIMIT 1"
|
||||
>>,
|
||||
server => mysql_server(),
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
|
||||
mysql_server() ->
|
||||
iolist_to_binary(io_lib:format("~s",[?MYSQL_HOST])).
|
||||
iolist_to_binary(io_lib:format("~s", [?MYSQL_HOST])).
|
||||
|
||||
start_apps(Apps) ->
|
||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||
|
|
|
@ -38,118 +38,150 @@ end_per_suite(_Config) ->
|
|||
ok.
|
||||
|
||||
t_gen_salt(_Config) ->
|
||||
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES]
|
||||
++ [#{name => bcrypt, salt_rounds => 10}],
|
||||
Algorithms =
|
||||
[#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES] ++
|
||||
[#{name => bcrypt, salt_rounds => 10}],
|
||||
|
||||
lists:foreach(
|
||||
fun(Algorithm) ->
|
||||
Salt = emqx_authn_password_hashing:gen_salt(Algorithm),
|
||||
ct:pal("gen_salt(~p): ~p", [Algorithm, Salt]),
|
||||
?assert(is_binary(Salt))
|
||||
end,
|
||||
Algorithms).
|
||||
fun(Algorithm) ->
|
||||
Salt = emqx_authn_password_hashing:gen_salt(Algorithm),
|
||||
ct:pal("gen_salt(~p): ~p", [Algorithm, Salt]),
|
||||
?assert(is_binary(Salt))
|
||||
end,
|
||||
Algorithms
|
||||
).
|
||||
|
||||
t_init(_Config) ->
|
||||
Algorithms = [#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES]
|
||||
++ [#{name => bcrypt, salt_rounds => 10}],
|
||||
Algorithms =
|
||||
[#{name => Type, salt_position => suffix} || Type <- ?SIMPLE_HASHES] ++
|
||||
[#{name => bcrypt, salt_rounds => 10}],
|
||||
|
||||
lists:foreach(
|
||||
fun(Algorithm) ->
|
||||
ok = emqx_authn_password_hashing:init(Algorithm)
|
||||
end,
|
||||
Algorithms).
|
||||
fun(Algorithm) ->
|
||||
ok = emqx_authn_password_hashing:init(Algorithm)
|
||||
end,
|
||||
Algorithms
|
||||
).
|
||||
|
||||
t_check_password(_Config) ->
|
||||
lists:foreach(
|
||||
fun test_check_password/1,
|
||||
hash_examples()).
|
||||
fun test_check_password/1,
|
||||
hash_examples()
|
||||
).
|
||||
|
||||
test_check_password(#{
|
||||
password_hash := Hash,
|
||||
salt := Salt,
|
||||
password := Password,
|
||||
password_hash_algorithm := Algorithm
|
||||
} = Sample) ->
|
||||
test_check_password(
|
||||
#{
|
||||
password_hash := Hash,
|
||||
salt := Salt,
|
||||
password := Password,
|
||||
password_hash_algorithm := Algorithm
|
||||
} = Sample
|
||||
) ->
|
||||
ct:pal("t_check_password sample: ~p", [Sample]),
|
||||
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password),
|
||||
false = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, <<"wrongpass">>).
|
||||
|
||||
t_hash(_Config) ->
|
||||
lists:foreach(
|
||||
fun test_hash/1,
|
||||
hash_examples()).
|
||||
fun test_hash/1,
|
||||
hash_examples()
|
||||
).
|
||||
|
||||
test_hash(#{password := Password,
|
||||
password_hash_algorithm := Algorithm
|
||||
} = Sample) ->
|
||||
test_hash(
|
||||
#{
|
||||
password := Password,
|
||||
password_hash_algorithm := Algorithm
|
||||
} = Sample
|
||||
) ->
|
||||
ct:pal("t_hash sample: ~p", [Sample]),
|
||||
{Hash, Salt} = emqx_authn_password_hashing:hash(Algorithm, Password),
|
||||
true = emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password).
|
||||
|
||||
hash_examples() ->
|
||||
[#{
|
||||
password_hash => <<"plainsalt">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"plain">>,
|
||||
password_hash_algorithm => #{name => plain,
|
||||
salt_position => suffix}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"md5">>,
|
||||
password_hash_algorithm => #{name => md5,
|
||||
salt_position => suffix}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"sha">>,
|
||||
password_hash_algorithm => #{name => sha,
|
||||
salt_position => prefix}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"sha256">>,
|
||||
password_hash_algorithm => #{name => sha256,
|
||||
salt_position => prefix}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"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 => <<"plainsalt">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"plain">>,
|
||||
password_hash_algorithm => #{
|
||||
name => plain,
|
||||
salt_position => suffix
|
||||
}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"md5">>,
|
||||
password_hash_algorithm => #{
|
||||
name => md5,
|
||||
salt_position => suffix
|
||||
}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"c665d4c0a9e5498806b7d9fd0b417d272853660e">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"sha">>,
|
||||
password_hash_algorithm => #{
|
||||
name => sha,
|
||||
salt_position => prefix
|
||||
}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
|
||||
salt => <<"salt">>,
|
||||
password => <<"sha256">>,
|
||||
password_hash_algorithm => #{
|
||||
name => sha256,
|
||||
salt_position => prefix
|
||||
}
|
||||
},
|
||||
#{
|
||||
password_hash => <<
|
||||
"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,
|
||||
salt_rounds => 10}
|
||||
},
|
||||
password_hash_algorithm => #{
|
||||
name => bcrypt,
|
||||
salt_rounds => 10
|
||||
}
|
||||
},
|
||||
|
||||
#{
|
||||
password_hash => <<"01dbee7f4a9e243e988b62c73cda935d"
|
||||
"a05378b93244ec8f48a99e61ad799d86">>,
|
||||
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||
password => <<"password">>,
|
||||
#{
|
||||
password_hash => <<
|
||||
"01dbee7f4a9e243e988b62c73cda935d"
|
||||
"a05378b93244ec8f48a99e61ad799d86"
|
||||
>>,
|
||||
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||
password => <<"password">>,
|
||||
|
||||
password_hash_algorithm => #{name => pbkdf2,
|
||||
iterations => 2,
|
||||
dk_length => 32,
|
||||
mac_fun => sha}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
|
||||
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||
password => <<"password">>,
|
||||
password_hash_algorithm => #{
|
||||
name => pbkdf2,
|
||||
iterations => 2,
|
||||
dk_length => 32,
|
||||
mac_fun => sha
|
||||
}
|
||||
},
|
||||
#{
|
||||
password_hash => <<"01dbee7f4a9e243e988b62c73cda935da05378b9">>,
|
||||
salt => <<"ATHENA.MIT.EDUraeburn">>,
|
||||
password => <<"password">>,
|
||||
|
||||
password_hash_algorithm => #{name => pbkdf2,
|
||||
iterations => 2,
|
||||
mac_fun => sha}
|
||||
}
|
||||
password_hash_algorithm => #{
|
||||
name => pbkdf2,
|
||||
iterations => 2,
|
||||
mac_fun => sha
|
||||
}
|
||||
}
|
||||
].
|
||||
|
|
|
@ -41,8 +41,9 @@ init_per_testcase(_, Config) ->
|
|||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
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 = start_apps([emqx_resource, emqx_connector]),
|
||||
{ok, _} = emqx_resource:create_local(
|
||||
?PGSQL_RESOURCE,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_pgsql,
|
||||
pgsql_config(),
|
||||
#{}),
|
||||
?PGSQL_RESOURCE,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_pgsql,
|
||||
pgsql_config(),
|
||||
#{}
|
||||
),
|
||||
Config;
|
||||
false ->
|
||||
{skip, no_pgsql}
|
||||
|
@ -72,8 +74,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
|
||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||
|
@ -86,8 +89,9 @@ t_create(_Config) ->
|
|||
AuthConfig = raw_pgsql_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_pgsql}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID).
|
||||
|
@ -97,131 +101,156 @@ t_create_invalid(_Config) ->
|
|||
|
||||
InvalidConfigs =
|
||||
[
|
||||
maps:without([server], AuthConfig),
|
||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||
AuthConfig#{password => <<"wrongpass">>},
|
||||
AuthConfig#{database => <<"wrongdatabase">>}
|
||||
maps:without([server], AuthConfig),
|
||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||
AuthConfig#{password => <<"wrongpass">>},
|
||||
AuthConfig#{database => <<"wrongdatabase">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(Config) ->
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs).
|
||||
fun(Config) ->
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs
|
||||
).
|
||||
|
||||
t_authenticate(_Config) ->
|
||||
ok = lists:foreach(
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
user_seeds()).
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
user_seeds()
|
||||
).
|
||||
|
||||
test_user_auth(#{credentials := Credentials0,
|
||||
config_params := SpecificConfigParams,
|
||||
result := Result}) ->
|
||||
test_user_auth(#{
|
||||
credentials := Credentials0,
|
||||
config_params := SpecificConfigParams,
|
||||
result := Result
|
||||
}) ->
|
||||
AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfigParams),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
Credentials = Credentials0#{
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
},
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
},
|
||||
|
||||
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL).
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
).
|
||||
|
||||
t_destroy(_Config) ->
|
||||
AuthConfig = raw_pgsql_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_pgsql, state := State}]}
|
||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
||||
{ok, [#{provider := emqx_authn_pgsql, state := State}]} =
|
||||
emqx_authentication:list_authenticators(?GLOBAL),
|
||||
|
||||
{ok, _} = emqx_authn_pgsql:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State),
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State
|
||||
),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
|
||||
% Authenticator should not be usable anymore
|
||||
?assertMatch(
|
||||
ignore,
|
||||
emqx_authn_pgsql:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State)).
|
||||
ignore,
|
||||
emqx_authn_pgsql:authenticate(
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State
|
||||
)
|
||||
).
|
||||
|
||||
t_update(_Config) ->
|
||||
CorrectConfig = raw_pgsql_auth_config(),
|
||||
IncorrectConfig =
|
||||
CorrectConfig#{
|
||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
||||
FROM users where username = ${username} LIMIT 0">>},
|
||||
query =>
|
||||
<<
|
||||
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||
" FROM users where username = ${username} LIMIT 0"
|
||||
>>
|
||||
},
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||
),
|
||||
|
||||
{error, not_authorized} = emqx_access_control:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}),
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}
|
||||
),
|
||||
|
||||
% We update with config with correct query, provider should update and work properly
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, CorrectConfig}),
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, CorrectConfig}
|
||||
),
|
||||
|
||||
{ok,_} = emqx_access_control:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}).
|
||||
{ok, _} = emqx_access_control:authenticate(
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}
|
||||
).
|
||||
|
||||
t_is_superuser(_Config) ->
|
||||
Config = raw_pgsql_auth_config(),
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
|
||||
Checks = [
|
||||
{is_superuser_str, "0", false},
|
||||
{is_superuser_str, "", false},
|
||||
{is_superuser_str, null, false},
|
||||
{is_superuser_str, "1", true},
|
||||
{is_superuser_str, "val", true},
|
||||
{is_superuser_str, "0", false},
|
||||
{is_superuser_str, "", false},
|
||||
{is_superuser_str, null, false},
|
||||
{is_superuser_str, "1", true},
|
||||
{is_superuser_str, "val", true},
|
||||
|
||||
{is_superuser_int, 0, false},
|
||||
{is_superuser_int, null, false},
|
||||
{is_superuser_int, 1, true},
|
||||
{is_superuser_int, 123, true},
|
||||
{is_superuser_int, 0, false},
|
||||
{is_superuser_int, null, false},
|
||||
{is_superuser_int, 1, true},
|
||||
{is_superuser_int, 123, true},
|
||||
|
||||
{is_superuser_bool, false, false},
|
||||
{is_superuser_bool, null, false},
|
||||
{is_superuser_bool, true, true}
|
||||
],
|
||||
{is_superuser_bool, false, false},
|
||||
{is_superuser_bool, null, false},
|
||||
{is_superuser_bool, true, true}
|
||||
],
|
||||
|
||||
lists:foreach(fun test_is_superuser/1, Checks).
|
||||
|
||||
|
@ -229,32 +258,36 @@ test_is_superuser({Field, Value, ExpectedValue}) ->
|
|||
{ok, _} = q("DELETE FROM users"),
|
||||
|
||||
UserData = #{
|
||||
username => "user",
|
||||
password_hash => "plainsalt",
|
||||
salt => "salt",
|
||||
Field => Value
|
||||
},
|
||||
username => "user",
|
||||
password_hash => "plainsalt",
|
||||
salt => "salt",
|
||||
Field => Value
|
||||
},
|
||||
|
||||
ok = create_user(UserData),
|
||||
|
||||
Query = "SELECT password_hash, salt, " ++ atom_to_list(Field) ++ " as is_superuser "
|
||||
"FROM users where username = ${username} LIMIT 1",
|
||||
Query =
|
||||
"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()),
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, Config}),
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, Config}
|
||||
),
|
||||
|
||||
Credentials = #{
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt,
|
||||
username => <<"user">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt,
|
||||
username => <<"user">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
|
||||
?assertEqual(
|
||||
{ok, #{is_superuser => ExpectedValue}},
|
||||
emqx_access_control:authenticate(Credentials)).
|
||||
{ok, #{is_superuser => ExpectedValue}},
|
||||
emqx_access_control:authenticate(Credentials)
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -262,167 +295,201 @@ test_is_superuser({Field, Value, ExpectedValue}) ->
|
|||
|
||||
raw_pgsql_auth_config() ->
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{name => <<"plain">>,
|
||||
salt_position => <<"suffix">>},
|
||||
enable => <<"true">>,
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"plain">>,
|
||||
salt_position => <<"suffix">>
|
||||
},
|
||||
enable => <<"true">>,
|
||||
|
||||
backend => <<"postgresql">>,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
backend => <<"postgresql">>,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
|
||||
query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
|
||||
FROM users where username = ${username} LIMIT 1">>,
|
||||
server => pgsql_server()
|
||||
}.
|
||||
query =>
|
||||
<<
|
||||
"SELECT password_hash, salt, is_superuser_str as is_superuser\n"
|
||||
" FROM users where username = ${username} LIMIT 1"
|
||||
>>,
|
||||
server => pgsql_server()
|
||||
}.
|
||||
|
||||
user_seeds() ->
|
||||
[#{data => #{
|
||||
username => "plain",
|
||||
password_hash => "plainsalt",
|
||||
salt => "salt",
|
||||
is_superuser_str => "1"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>},
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => true}}
|
||||
},
|
||||
|
||||
#{data => #{
|
||||
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
|
||||
[
|
||||
#{
|
||||
data => #{
|
||||
username => "plain",
|
||||
password_hash => "plainsalt",
|
||||
salt => "salt",
|
||||
is_superuser_str => "1"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => true}}
|
||||
},
|
||||
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">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||
is_superuser_int => 0
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||
FROM users where username = ${username} LIMIT 1">>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
#{
|
||||
data => #{
|
||||
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 => <<"bcrypt0">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||
is_superuser_str => "0"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
% clientid variable & username credentials
|
||||
query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
|
||||
FROM users where username = ${clientid} LIMIT 1">>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {error,not_authorized}
|
||||
},
|
||||
#{
|
||||
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\n"
|
||||
" FROM users where username = ${clientid} LIMIT 1"
|
||||
>>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"sha256">>,
|
||||
salt_position => <<"prefix">>
|
||||
}
|
||||
},
|
||||
result => {ok, #{is_superuser => true}}
|
||||
},
|
||||
|
||||
#{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
|
||||
FROM users where username = ${username} LIMIT 1">>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {error,not_authorized}
|
||||
},
|
||||
#{
|
||||
data => #{
|
||||
username => <<"bcrypt">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||
is_superuser_int => 0
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
query =>
|
||||
<<
|
||||
"SELECT password_hash, salt, is_superuser_int as is_superuser\n"
|
||||
" FROM users where username = ${username} LIMIT 1"
|
||||
>>,
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
|
||||
#{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}
|
||||
}
|
||||
#{
|
||||
data => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
|
||||
salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
|
||||
is_superuser_str => "0"
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt0">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
config_params => #{
|
||||
% clientid variable & username credentials
|
||||
query =>
|
||||
<<
|
||||
"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() ->
|
||||
ok = drop_seeds(),
|
||||
{ok, _, _} = q("CREATE TABLE users(
|
||||
username varchar(255),
|
||||
password_hash varchar(255),
|
||||
salt varchar(255),
|
||||
is_superuser_str varchar(255),
|
||||
is_superuser_int smallint,
|
||||
is_superuser_bool boolean)"),
|
||||
{ok, _, _} = q(
|
||||
"CREATE TABLE users(\n"
|
||||
" username varchar(255),\n"
|
||||
" password_hash varchar(255),\n"
|
||||
" salt varchar(255),\n"
|
||||
" is_superuser_str varchar(255),\n"
|
||||
" is_superuser_int smallint,\n"
|
||||
" is_superuser_bool boolean)"
|
||||
),
|
||||
|
||||
lists:foreach(
|
||||
fun(#{data := Values}) ->
|
||||
ok = create_user(Values)
|
||||
end,
|
||||
user_seeds()).
|
||||
fun(#{data := Values}) ->
|
||||
ok = create_user(Values)
|
||||
end,
|
||||
user_seeds()
|
||||
).
|
||||
|
||||
create_user(Values) ->
|
||||
Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int, is_superuser_bool],
|
||||
|
||||
InsertQuery = "INSERT INTO users(username, password_hash, salt,"
|
||||
"is_superuser_str, is_superuser_int, is_superuser_bool) "
|
||||
"VALUES($1, $2, $3, $4, $5, $6)",
|
||||
InsertQuery =
|
||||
"INSERT INTO users(username, password_hash, salt,"
|
||||
"is_superuser_str, is_superuser_int, is_superuser_bool) "
|
||||
"VALUES($1, $2, $3, $4, $5, $6)",
|
||||
|
||||
Params = [maps:get(F, Values, null) || F <- Fields],
|
||||
{ok, 1} = q(InsertQuery, Params),
|
||||
|
@ -430,30 +497,33 @@ create_user(Values) ->
|
|||
|
||||
q(Sql) ->
|
||||
emqx_resource:query(
|
||||
?PGSQL_RESOURCE,
|
||||
{query, Sql}).
|
||||
?PGSQL_RESOURCE,
|
||||
{query, Sql}
|
||||
).
|
||||
|
||||
q(Sql, Params) ->
|
||||
emqx_resource:query(
|
||||
?PGSQL_RESOURCE,
|
||||
{query, Sql, Params}).
|
||||
?PGSQL_RESOURCE,
|
||||
{query, Sql, Params}
|
||||
).
|
||||
|
||||
drop_seeds() ->
|
||||
{ok, _, _} = q("DROP TABLE IF EXISTS users"),
|
||||
ok.
|
||||
|
||||
pgsql_server() ->
|
||||
iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])).
|
||||
iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
|
||||
|
||||
pgsql_config() ->
|
||||
#{auto_reconnect => true,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
pool_size => 8,
|
||||
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
|
||||
ssl => #{enable => false}
|
||||
}.
|
||||
#{
|
||||
auto_reconnect => true,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
pool_size => 8,
|
||||
server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
|
||||
ssl => #{enable => false}
|
||||
}.
|
||||
|
||||
start_apps(Apps) ->
|
||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||
|
|
|
@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
|
|||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
Config.
|
||||
|
||||
init_per_suite(Config) ->
|
||||
|
@ -56,8 +57,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||
|
||||
|
@ -70,38 +72,53 @@ t_create(_Config) ->
|
|||
%% -starttls postgres -connect authn-server:5432 \
|
||||
%% -cert client.crt -key client.key -CAfile ca.crt
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_pgsql_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]})).
|
||||
{ok, _},
|
||||
create_pgsql_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-RSA-AES256-GCM-SHA384">>]
|
||||
}
|
||||
)
|
||||
).
|
||||
|
||||
t_create_invalid(_Config) ->
|
||||
|
||||
%% invalid server_name
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_pgsql_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>})),
|
||||
{ok, _},
|
||||
create_pgsql_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>
|
||||
}
|
||||
)
|
||||
),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
%% incompatible versions
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_pgsql_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>]})),
|
||||
{ok, _},
|
||||
create_pgsql_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>]
|
||||
}
|
||||
)
|
||||
),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
%% incompatible ciphers
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_pgsql_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]})).
|
||||
{ok, _},
|
||||
create_pgsql_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.2">>],
|
||||
<<"ciphers">> => [<<"ECDHE-ECDSA-AES128-GCM-SHA256">>]
|
||||
}
|
||||
)
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -113,26 +130,29 @@ create_pgsql_auth_with_ssl_opts(SpecificSSLOpts) ->
|
|||
|
||||
raw_pgsql_auth_config(SpecificSSLOpts) ->
|
||||
SSLOpts = maps:merge(
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}),
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}
|
||||
),
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{name => <<"plain">>,
|
||||
salt_position => <<"suffix">>},
|
||||
enable => <<"true">>,
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"plain">>,
|
||||
salt_position => <<"suffix">>
|
||||
},
|
||||
enable => <<"true">>,
|
||||
|
||||
backend => <<"postgresql">>,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
backend => <<"postgresql">>,
|
||||
database => <<"mqtt">>,
|
||||
username => <<"root">>,
|
||||
password => <<"public">>,
|
||||
|
||||
query => <<"SELECT 1">>,
|
||||
server => pgsql_server(),
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
query => <<"SELECT 1">>,
|
||||
server => pgsql_server(),
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
|
||||
pgsql_server() ->
|
||||
iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])).
|
||||
iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
|
||||
|
||||
start_apps(Apps) ->
|
||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||
|
|
|
@ -40,8 +40,9 @@ init_per_testcase(_, Config) ->
|
|||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
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 = start_apps([emqx_resource, emqx_connector]),
|
||||
{ok, _} = emqx_resource:create_local(
|
||||
?REDIS_RESOURCE,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_redis,
|
||||
redis_config(),
|
||||
#{}),
|
||||
?REDIS_RESOURCE,
|
||||
?RESOURCE_GROUP,
|
||||
emqx_connector_redis,
|
||||
redis_config(),
|
||||
#{}
|
||||
),
|
||||
Config;
|
||||
false ->
|
||||
{skip, no_redis}
|
||||
|
@ -71,8 +73,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
ok = emqx_resource:remove_local(?REDIS_RESOURCE),
|
||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||
|
@ -86,8 +89,9 @@ t_create(_Config) ->
|
|||
|
||||
AuthConfig = raw_redis_auth_config(),
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_redis}]} = emqx_authentication:list_authenticators(?GLOBAL).
|
||||
|
||||
|
@ -95,126 +99,152 @@ t_create_invalid(_Config) ->
|
|||
AuthConfig = raw_redis_auth_config(),
|
||||
InvalidConfigs =
|
||||
[
|
||||
AuthConfig#{
|
||||
cmd => <<"MGET password_hash:${username} salt:${username}">>},
|
||||
AuthConfig#{
|
||||
cmd => <<"HMGET mqtt_user:${username} password_hash invalid_field">>},
|
||||
AuthConfig#{
|
||||
cmd => <<"HMGET mqtt_user:${username} salt is_superuser">>}
|
||||
AuthConfig#{
|
||||
cmd => <<"MGET password_hash:${username} salt:${username}">>
|
||||
},
|
||||
AuthConfig#{
|
||||
cmd => <<"HMGET mqtt_user:${username} password_hash invalid_field">>
|
||||
},
|
||||
AuthConfig#{
|
||||
cmd => <<"HMGET mqtt_user:${username} salt is_superuser">>
|
||||
}
|
||||
],
|
||||
lists:foreach(
|
||||
fun(Config) ->
|
||||
{error, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
fun(Config) ->
|
||||
{error, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs),
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs
|
||||
),
|
||||
|
||||
InvalidConfigs1 =
|
||||
[
|
||||
maps:without([server], AuthConfig),
|
||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||
AuthConfig#{password => <<"wrongpass">>},
|
||||
AuthConfig#{database => <<"5678">>}
|
||||
maps:without([server], AuthConfig),
|
||||
AuthConfig#{server => <<"unknownhost:3333">>},
|
||||
AuthConfig#{password => <<"wrongpass">>},
|
||||
AuthConfig#{database => <<"5678">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(Config) ->
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs1).
|
||||
fun(Config) ->
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
emqx_authn_test_lib:delete_config(?ResourceID),
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
|
||||
end,
|
||||
InvalidConfigs1
|
||||
).
|
||||
|
||||
t_authenticate(_Config) ->
|
||||
ok = lists:foreach(
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
user_seeds()).
|
||||
fun(Sample) ->
|
||||
ct:pal("test_user_auth sample: ~p", [Sample]),
|
||||
test_user_auth(Sample)
|
||||
end,
|
||||
user_seeds()
|
||||
).
|
||||
|
||||
test_user_auth(#{credentials := Credentials0,
|
||||
config_params := SpecificConfigParams,
|
||||
result := Result}) ->
|
||||
test_user_auth(#{
|
||||
credentials := Credentials0,
|
||||
config_params := SpecificConfigParams,
|
||||
result := Result
|
||||
}) ->
|
||||
AuthConfig = maps:merge(raw_redis_auth_config(), SpecificConfigParams),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
Credentials = Credentials0#{
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
},
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
},
|
||||
|
||||
?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL).
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
).
|
||||
|
||||
t_destroy(_Config) ->
|
||||
AuthConfig = raw_redis_auth_config(),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, AuthConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_authn_redis, state := State}]}
|
||||
= emqx_authentication:list_authenticators(?GLOBAL),
|
||||
{ok, [#{provider := emqx_authn_redis, state := State}]} =
|
||||
emqx_authentication:list_authenticators(?GLOBAL),
|
||||
|
||||
{ok, _} = emqx_authn_redis:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State),
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State
|
||||
),
|
||||
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
|
||||
% Authenticator should not be usable anymore
|
||||
?assertMatch(
|
||||
ignore,
|
||||
emqx_authn_redis:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State)).
|
||||
ignore,
|
||||
emqx_authn_redis:authenticate(
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
State
|
||||
)
|
||||
).
|
||||
|
||||
t_update(_Config) ->
|
||||
CorrectConfig = raw_redis_auth_config(),
|
||||
IncorrectConfig =
|
||||
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(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, IncorrectConfig}
|
||||
),
|
||||
|
||||
{error, not_authorized} = emqx_access_control:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}),
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}
|
||||
),
|
||||
|
||||
% We update with config with correct query, provider should update and work properly
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:redis">>, CorrectConfig}),
|
||||
?PATH,
|
||||
{update_authenticator, ?GLOBAL, <<"password_based:redis">>, CorrectConfig}
|
||||
),
|
||||
|
||||
{ok,_} = emqx_access_control:authenticate(
|
||||
#{username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}).
|
||||
{ok, _} = emqx_access_control:authenticate(
|
||||
#{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>,
|
||||
listener => 'tcp:default',
|
||||
protocol => mqtt
|
||||
}
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -222,194 +252,218 @@ t_update(_Config) ->
|
|||
|
||||
raw_redis_auth_config() ->
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{name => <<"plain">>,
|
||||
salt_position => <<"suffix">>},
|
||||
enable => <<"true">>,
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"plain">>,
|
||||
salt_position => <<"suffix">>
|
||||
},
|
||||
enable => <<"true">>,
|
||||
|
||||
backend => <<"redis">>,
|
||||
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
||||
database => <<"1">>,
|
||||
password => <<"public">>,
|
||||
server => redis_server()
|
||||
}.
|
||||
backend => <<"redis">>,
|
||||
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
||||
database => <<"1">>,
|
||||
password => <<"public">>,
|
||||
server => redis_server()
|
||||
}.
|
||||
|
||||
user_seeds() ->
|
||||
[#{data => #{
|
||||
password_hash => <<"plainsalt">>,
|
||||
salt => <<"salt">>,
|
||||
is_superuser => <<"1">>
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>},
|
||||
key => <<"mqtt_user:plain">>,
|
||||
config_params => #{},
|
||||
result => {ok,#{is_superuser => true}}
|
||||
},
|
||||
|
||||
#{data => #{
|
||||
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">>
|
||||
[
|
||||
#{
|
||||
data => #{
|
||||
password_hash => <<"plainsalt">>,
|
||||
salt => <<"salt">>,
|
||||
is_superuser => <<"1">>
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"plain">>,
|
||||
password => <<"plain">>
|
||||
},
|
||||
key => <<"mqtt_user:plain">>,
|
||||
config_params => #{},
|
||||
result => {ok, #{is_superuser => true}}
|
||||
},
|
||||
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 =>
|
||||
<<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
|
||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||
is_superuser => <<"0">>
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
key => <<"mqtt_user:bcrypt">>,
|
||||
config_params => #{
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok,#{is_superuser => false}}
|
||||
},
|
||||
#{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 => <<"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 =>
|
||||
<<"$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 =>
|
||||
<<"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 => #{
|
||||
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}
|
||||
}
|
||||
salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
|
||||
is_superuser => <<"0">>
|
||||
},
|
||||
credentials => #{
|
||||
username => <<"bcrypt">>,
|
||||
password => <<"bcrypt">>
|
||||
},
|
||||
key => <<"mqtt_user:bcrypt">>,
|
||||
config_params => #{
|
||||
password_hash_algorithm => #{name => <<"bcrypt">>}
|
||||
},
|
||||
result => {ok, #{is_superuser => false}}
|
||||
},
|
||||
#{
|
||||
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() ->
|
||||
ok = drop_seeds(),
|
||||
lists:foreach(
|
||||
fun(#{key := UserKey, data := Values}) ->
|
||||
lists:foreach(fun({Key, Value}) ->
|
||||
q(["HSET", UserKey, atom_to_list(Key), Value])
|
||||
end,
|
||||
maps:to_list(Values))
|
||||
end,
|
||||
user_seeds()).
|
||||
fun(#{key := UserKey, data := Values}) ->
|
||||
lists:foreach(
|
||||
fun({Key, Value}) ->
|
||||
q(["HSET", UserKey, atom_to_list(Key), Value])
|
||||
end,
|
||||
maps:to_list(Values)
|
||||
)
|
||||
end,
|
||||
user_seeds()
|
||||
).
|
||||
|
||||
q(Command) ->
|
||||
emqx_resource:query(
|
||||
?REDIS_RESOURCE,
|
||||
{cmd, Command}).
|
||||
?REDIS_RESOURCE,
|
||||
{cmd, Command}
|
||||
).
|
||||
|
||||
drop_seeds() ->
|
||||
lists:foreach(
|
||||
fun(#{key := UserKey}) ->
|
||||
q(["DEL", UserKey])
|
||||
end,
|
||||
user_seeds()).
|
||||
fun(#{key := UserKey}) ->
|
||||
q(["DEL", UserKey])
|
||||
end,
|
||||
user_seeds()
|
||||
).
|
||||
|
||||
redis_server() ->
|
||||
iolist_to_binary(io_lib:format("~s",[?REDIS_HOST])).
|
||||
iolist_to_binary(io_lib:format("~s", [?REDIS_HOST])).
|
||||
|
||||
redis_config() ->
|
||||
#{auto_reconnect => true,
|
||||
database => 1,
|
||||
pool_size => 8,
|
||||
redis_type => single,
|
||||
password => "public",
|
||||
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
|
||||
ssl => #{enable => false}
|
||||
}.
|
||||
#{
|
||||
auto_reconnect => true,
|
||||
database => 1,
|
||||
pool_size => 8,
|
||||
redis_type => single,
|
||||
password => "public",
|
||||
server => {?REDIS_HOST, ?REDIS_DEFAULT_PORT},
|
||||
ssl => #{enable => false}
|
||||
}.
|
||||
|
||||
start_apps(Apps) ->
|
||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||
|
|
|
@ -39,8 +39,9 @@ init_per_testcase(_, Config) ->
|
|||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
emqx_authentication:initialize_authentication(?GLOBAL, []),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
Config.
|
||||
|
||||
init_per_suite(Config) ->
|
||||
|
@ -56,8 +57,9 @@ init_per_suite(Config) ->
|
|||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
ok = stop_apps([emqx_resource, emqx_connector]),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
|
||||
|
||||
|
@ -67,39 +69,55 @@ end_per_suite(_Config) ->
|
|||
|
||||
t_create(_Config) ->
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_redis_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.3">>],
|
||||
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]})).
|
||||
{ok, _},
|
||||
create_redis_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.3">>],
|
||||
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]
|
||||
}
|
||||
)
|
||||
).
|
||||
|
||||
t_create_invalid(_Config) ->
|
||||
%% invalid server_name
|
||||
?assertMatch(
|
||||
{ok, _},
|
||||
create_redis_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.3">>],
|
||||
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]})),
|
||||
{ok, _},
|
||||
create_redis_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server-unknown-host">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.3">>],
|
||||
<<"ciphers">> => [<<"TLS_CHACHA20_POLY1305_SHA256">>]
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
%% incompatible versions
|
||||
?assertMatch(
|
||||
{error, _},
|
||||
create_redis_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>, <<"tlsv1.2">>]})),
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.1">>, <<"tlsv1.2">>]
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
%% incompatible ciphers
|
||||
?assertMatch(
|
||||
{error, _},
|
||||
create_redis_auth_with_ssl_opts(
|
||||
#{<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.3">>],
|
||||
<<"ciphers">> => [<<"TLS_AES_128_GCM_SHA256">>]})).
|
||||
{error, _},
|
||||
create_redis_auth_with_ssl_opts(
|
||||
#{
|
||||
<<"server_name_indication">> => <<"authn-server">>,
|
||||
<<"verify">> => <<"verify_peer">>,
|
||||
<<"versions">> => [<<"tlsv1.3">>],
|
||||
<<"ciphers">> => [<<"TLS_AES_128_GCM_SHA256">>]
|
||||
}
|
||||
)
|
||||
).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
@ -111,24 +129,27 @@ create_redis_auth_with_ssl_opts(SpecificSSLOpts) ->
|
|||
|
||||
raw_redis_auth_config(SpecificSSLOpts) ->
|
||||
SSLOpts = maps:merge(
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}),
|
||||
emqx_authn_test_lib:client_ssl_cert_opts(),
|
||||
#{enable => <<"true">>}
|
||||
),
|
||||
#{
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{name => <<"plain">>,
|
||||
salt_position => <<"suffix">>},
|
||||
enable => <<"true">>,
|
||||
mechanism => <<"password_based">>,
|
||||
password_hash_algorithm => #{
|
||||
name => <<"plain">>,
|
||||
salt_position => <<"suffix">>
|
||||
},
|
||||
enable => <<"true">>,
|
||||
|
||||
backend => <<"redis">>,
|
||||
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
||||
database => <<"1">>,
|
||||
password => <<"public">>,
|
||||
server => redis_server(),
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
backend => <<"redis">>,
|
||||
cmd => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
|
||||
database => <<"1">>,
|
||||
password => <<"public">>,
|
||||
server => redis_server(),
|
||||
ssl => maps:merge(SSLOpts, SpecificSSLOpts)
|
||||
}.
|
||||
|
||||
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) ->
|
||||
lists:foreach(fun application:ensure_all_started/1, Apps).
|
||||
|
|
|
@ -36,16 +36,19 @@ jwt_example() ->
|
|||
|
||||
delete_authenticators(Path, Chain) ->
|
||||
case emqx_authentication:list_authenticators(Chain) of
|
||||
{error, _} -> ok;
|
||||
{error, _} ->
|
||||
ok;
|
||||
{ok, Authenticators} ->
|
||||
lists:foreach(
|
||||
fun(#{id := ID}) ->
|
||||
emqx:update_config(
|
||||
Path,
|
||||
{delete_authenticator, Chain, ID},
|
||||
#{rawconf_with_defaults => true})
|
||||
#{rawconf_with_defaults => true}
|
||||
)
|
||||
end,
|
||||
Authenticators)
|
||||
Authenticators
|
||||
)
|
||||
end.
|
||||
|
||||
delete_config(ID) ->
|
||||
|
@ -53,10 +56,13 @@ delete_config(ID) ->
|
|||
emqx:update_config(
|
||||
[authentication],
|
||||
{delete_authenticator, ?GLOBAL, ID},
|
||||
#{rawconf_with_defaults => false}).
|
||||
#{rawconf_with_defaults => false}
|
||||
).
|
||||
|
||||
client_ssl_cert_opts() ->
|
||||
Dir = code:lib_dir(emqx_authn, test),
|
||||
#{keyfile => filename:join([Dir, "data/certs", "client.key"]),
|
||||
certfile => filename:join([Dir, "data/certs", "client.crt"]),
|
||||
cacertfile => filename:join([Dir, "data/certs", "ca.crt"])}.
|
||||
#{
|
||||
keyfile => filename:join([Dir, "data/certs", "client.key"]),
|
||||
certfile => filename:join([Dir, "data/certs", "client.crt"]),
|
||||
cacertfile => filename:join([Dir, "data/certs", "ca.crt"])
|
||||
}.
|
||||
|
|
|
@ -26,14 +26,16 @@
|
|||
|
||||
-define(PATH, [authentication]).
|
||||
|
||||
-define(USER_MAP, #{user_id := _,
|
||||
is_superuser := _}).
|
||||
-define(USER_MAP, #{
|
||||
user_id := _,
|
||||
is_superuser := _
|
||||
}).
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
_ = application:load(emqx_conf),
|
||||
_ = application:load(emqx_conf),
|
||||
ok = emqx_common_test_helpers:start_apps([emqx_authn]),
|
||||
Config.
|
||||
|
||||
|
@ -44,8 +46,9 @@ init_per_testcase(_Case, Config) ->
|
|||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
mria:clear_table(emqx_enhanced_authn_scram_mnesia),
|
||||
emqx_authn_test_lib:delete_authenticators(
|
||||
[authentication],
|
||||
?GLOBAL),
|
||||
[authentication],
|
||||
?GLOBAL
|
||||
),
|
||||
Config.
|
||||
|
||||
end_per_testcase(_Case, Config) ->
|
||||
|
@ -64,23 +67,25 @@ t_create(_Config) ->
|
|||
},
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, ValidConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, ValidConfig}
|
||||
),
|
||||
|
||||
{ok, [#{provider := emqx_enhanced_authn_scram_mnesia}]}
|
||||
= emqx_authentication:list_authenticators(?GLOBAL).
|
||||
{ok, [#{provider := emqx_enhanced_authn_scram_mnesia}]} =
|
||||
emqx_authentication:list_authenticators(?GLOBAL).
|
||||
|
||||
t_create_invalid(_Config) ->
|
||||
InvalidConfig = #{
|
||||
<<"mechanism">> => <<"scram">>,
|
||||
<<"backend">> => <<"built_in_database">>,
|
||||
<<"algorithm">> => <<"sha271828">>,
|
||||
<<"iteration_count">> => <<"4096">>
|
||||
},
|
||||
<<"mechanism">> => <<"scram">>,
|
||||
<<"backend">> => <<"built_in_database">>,
|
||||
<<"algorithm">> => <<"sha271828">>,
|
||||
<<"iteration_count">> => <<"4096">>
|
||||
},
|
||||
|
||||
{error, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, InvalidConfig}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, InvalidConfig}
|
||||
),
|
||||
|
||||
{ok, []} = emqx_authentication:list_authenticators(?GLOBAL).
|
||||
|
||||
|
@ -96,39 +101,47 @@ t_authenticate(_Config) ->
|
|||
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
||||
|
||||
ConnectPacket = ?CONNECT_PACKET(
|
||||
#mqtt_packet_connect{
|
||||
proto_ver = ?MQTT_PROTO_V5,
|
||||
properties = #{
|
||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFirstMessage
|
||||
}
|
||||
}),
|
||||
#mqtt_packet_connect{
|
||||
proto_ver = ?MQTT_PROTO_V5,
|
||||
properties = #{
|
||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFirstMessage
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
||||
|
||||
?AUTH_PACKET(
|
||||
?RC_CONTINUE_AUTHENTICATION,
|
||||
#{'Authentication-Data' := ServerFirstMessage}) = receive_packet(),
|
||||
?RC_CONTINUE_AUTHENTICATION,
|
||||
#{'Authentication-Data' := ServerFirstMessage}
|
||||
) = receive_packet(),
|
||||
|
||||
{continue, ClientFinalMessage, ClientCache} =
|
||||
esasl_scram:check_server_first_message(
|
||||
ServerFirstMessage,
|
||||
#{client_first_message => ClientFirstMessage,
|
||||
password => Password,
|
||||
algorithm => Algorithm}
|
||||
#{
|
||||
client_first_message => ClientFirstMessage,
|
||||
password => Password,
|
||||
algorithm => Algorithm
|
||||
}
|
||||
),
|
||||
|
||||
AuthContinuePacket = ?AUTH_PACKET(
|
||||
?RC_CONTINUE_AUTHENTICATION,
|
||||
#{'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFinalMessage}),
|
||||
?RC_CONTINUE_AUTHENTICATION,
|
||||
#{
|
||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFinalMessage
|
||||
}
|
||||
),
|
||||
|
||||
ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket),
|
||||
|
||||
?CONNACK_PACKET(
|
||||
?RC_SUCCESS,
|
||||
_,
|
||||
#{'Authentication-Data' := ServerFinalMessage}) = receive_packet(),
|
||||
?RC_SUCCESS,
|
||||
_,
|
||||
#{'Authentication-Data' := ServerFinalMessage}
|
||||
) = receive_packet(),
|
||||
|
||||
ok = esasl_scram:check_server_final_message(
|
||||
ServerFinalMessage, ClientCache#{algorithm => Algorithm}
|
||||
|
@ -146,13 +159,14 @@ t_authenticate_bad_username(_Config) ->
|
|||
ClientFirstMessage = esasl_scram:client_first_message(<<"badusername">>),
|
||||
|
||||
ConnectPacket = ?CONNECT_PACKET(
|
||||
#mqtt_packet_connect{
|
||||
proto_ver = ?MQTT_PROTO_V5,
|
||||
properties = #{
|
||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFirstMessage
|
||||
}
|
||||
}),
|
||||
#mqtt_packet_connect{
|
||||
proto_ver = ?MQTT_PROTO_V5,
|
||||
properties = #{
|
||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFirstMessage
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
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),
|
||||
|
||||
ConnectPacket = ?CONNECT_PACKET(
|
||||
#mqtt_packet_connect{
|
||||
proto_ver = ?MQTT_PROTO_V5,
|
||||
properties = #{
|
||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFirstMessage
|
||||
}
|
||||
}),
|
||||
#mqtt_packet_connect{
|
||||
proto_ver = ?MQTT_PROTO_V5,
|
||||
properties = #{
|
||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFirstMessage
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket),
|
||||
|
||||
?AUTH_PACKET(
|
||||
?RC_CONTINUE_AUTHENTICATION,
|
||||
#{'Authentication-Data' := ServerFirstMessage}) = receive_packet(),
|
||||
?RC_CONTINUE_AUTHENTICATION,
|
||||
#{'Authentication-Data' := ServerFirstMessage}
|
||||
) = receive_packet(),
|
||||
|
||||
{continue, ClientFinalMessage, _ClientCache} =
|
||||
esasl_scram:check_server_first_message(
|
||||
ServerFirstMessage,
|
||||
#{client_first_message => ClientFirstMessage,
|
||||
password => <<"badpassword">>,
|
||||
algorithm => Algorithm}
|
||||
#{
|
||||
client_first_message => ClientFirstMessage,
|
||||
password => <<"badpassword">>,
|
||||
algorithm => Algorithm
|
||||
}
|
||||
),
|
||||
|
||||
AuthContinuePacket = ?AUTH_PACKET(
|
||||
?RC_CONTINUE_AUTHENTICATION,
|
||||
#{'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFinalMessage}),
|
||||
?RC_CONTINUE_AUTHENTICATION,
|
||||
#{
|
||||
'Authentication-Method' => <<"SCRAM-SHA-512">>,
|
||||
'Authentication-Data' => ClientFinalMessage
|
||||
}
|
||||
),
|
||||
|
||||
ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket),
|
||||
|
||||
|
@ -218,7 +239,7 @@ t_destroy(_) ->
|
|||
ok = emqx_enhanced_authn_scram_mnesia:destroy(State0),
|
||||
|
||||
{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).
|
||||
|
||||
t_add_user(_) ->
|
||||
|
@ -248,12 +269,14 @@ t_update_user(_) ->
|
|||
{ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State),
|
||||
{ok, #{is_superuser := false}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State),
|
||||
|
||||
{ok,
|
||||
#{user_id := <<"u">>,
|
||||
is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:update_user(
|
||||
<<"u">>,
|
||||
#{password => <<"p1">>, is_superuser => true},
|
||||
State),
|
||||
{ok, #{
|
||||
user_id := <<"u">>,
|
||||
is_superuser := true
|
||||
}} = emqx_enhanced_authn_scram_mnesia:update_user(
|
||||
<<"u">>,
|
||||
#{password => <<"p1">>, is_superuser => true},
|
||||
State
|
||||
),
|
||||
|
||||
{ok, #{is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State).
|
||||
|
||||
|
@ -261,29 +284,47 @@ t_list_users(_) ->
|
|||
Config = config(),
|
||||
{ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config),
|
||||
|
||||
Users = [#{user_id => <<"u1">>, password => <<"p">>},
|
||||
#{user_id => <<"u2">>, password => <<"p">>},
|
||||
#{user_id => <<"u3">>, password => <<"p">>}],
|
||||
Users = [
|
||||
#{user_id => <<"u1">>, password => <<"p">>},
|
||||
#{user_id => <<"u2">>, password => <<"p">>},
|
||||
#{user_id => <<"u3">>, password => <<"p">>}
|
||||
],
|
||||
|
||||
lists:foreach(
|
||||
fun(U) -> {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(U, State) end,
|
||||
Users),
|
||||
fun(U) -> {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(U, State) end,
|
||||
Users
|
||||
),
|
||||
|
||||
#{data := [?USER_MAP, ?USER_MAP],
|
||||
meta := #{page := 1, limit := 2, count := 3}} = emqx_enhanced_authn_scram_mnesia:list_users(
|
||||
#{<<"page">> => 1, <<"limit">> => 2},
|
||||
State),
|
||||
#{data := [?USER_MAP],
|
||||
meta := #{page := 2, limit := 2, count := 3}} = emqx_enhanced_authn_scram_mnesia:list_users(
|
||||
#{<<"page">> => 2, <<"limit">> => 2},
|
||||
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).
|
||||
#{
|
||||
data := [?USER_MAP, ?USER_MAP],
|
||||
meta := #{page := 1, limit := 2, count := 3}
|
||||
} = emqx_enhanced_authn_scram_mnesia:list_users(
|
||||
#{<<"page">> => 1, <<"limit">> => 2},
|
||||
State
|
||||
),
|
||||
#{
|
||||
data := [?USER_MAP],
|
||||
meta := #{page := 2, limit := 2, count := 3}
|
||||
} = emqx_enhanced_authn_scram_mnesia:list_users(
|
||||
#{<<"page">> => 2, <<"limit">> => 2},
|
||||
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) ->
|
||||
ok = test_is_superuser(#{is_superuser => false}, false),
|
||||
|
@ -297,36 +338,44 @@ test_is_superuser(UserInfo, ExpectedIsSuperuser) ->
|
|||
Username = <<"u">>,
|
||||
Password = <<"p">>,
|
||||
|
||||
UserInfo0 = UserInfo#{user_id => Username,
|
||||
password => Password},
|
||||
UserInfo0 = UserInfo#{
|
||||
user_id => Username,
|
||||
password => Password
|
||||
},
|
||||
|
||||
{ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(UserInfo0, State),
|
||||
|
||||
ClientFirstMessage = esasl_scram:client_first_message(Username),
|
||||
|
||||
{continue, ServerFirstMessage, ServerCache}
|
||||
= emqx_enhanced_authn_scram_mnesia:authenticate(
|
||||
#{auth_method => <<"SCRAM-SHA-512">>,
|
||||
auth_data => ClientFirstMessage,
|
||||
auth_cache => #{}
|
||||
},
|
||||
State),
|
||||
{continue, ServerFirstMessage, ServerCache} =
|
||||
emqx_enhanced_authn_scram_mnesia:authenticate(
|
||||
#{
|
||||
auth_method => <<"SCRAM-SHA-512">>,
|
||||
auth_data => ClientFirstMessage,
|
||||
auth_cache => #{}
|
||||
},
|
||||
State
|
||||
),
|
||||
|
||||
{continue, ClientFinalMessage, ClientCache} =
|
||||
esasl_scram:check_server_first_message(
|
||||
ServerFirstMessage,
|
||||
#{client_first_message => ClientFirstMessage,
|
||||
password => Password,
|
||||
algorithm => sha512}
|
||||
#{
|
||||
client_first_message => ClientFirstMessage,
|
||||
password => Password,
|
||||
algorithm => sha512
|
||||
}
|
||||
),
|
||||
|
||||
{ok, UserInfo1, ServerFinalMessage}
|
||||
= emqx_enhanced_authn_scram_mnesia:authenticate(
|
||||
#{auth_method => <<"SCRAM-SHA-512">>,
|
||||
auth_data => ClientFinalMessage,
|
||||
auth_cache => ServerCache
|
||||
},
|
||||
State),
|
||||
{ok, UserInfo1, ServerFinalMessage} =
|
||||
emqx_enhanced_authn_scram_mnesia:authenticate(
|
||||
#{
|
||||
auth_method => <<"SCRAM-SHA-512">>,
|
||||
auth_data => ClientFinalMessage,
|
||||
auth_cache => ServerCache
|
||||
},
|
||||
State
|
||||
),
|
||||
|
||||
ok = esasl_scram:check_server_final_message(
|
||||
ServerFinalMessage, ClientCache#{algorithm => sha512}
|
||||
|
@ -336,18 +385,17 @@ test_is_superuser(UserInfo, ExpectedIsSuperuser) ->
|
|||
|
||||
ok = emqx_enhanced_authn_scram_mnesia:destroy(State).
|
||||
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
config() ->
|
||||
#{
|
||||
mechanism => <<"scram">>,
|
||||
backend => <<"built_in_database">>,
|
||||
algorithm => sha512,
|
||||
iteration_count => 4096
|
||||
}.
|
||||
mechanism => <<"scram">>,
|
||||
backend => <<"built_in_database">>,
|
||||
algorithm => sha512,
|
||||
iteration_count => 4096
|
||||
}.
|
||||
|
||||
raw_config(Algorithm) ->
|
||||
#{
|
||||
|
@ -361,14 +409,16 @@ init_auth(Username, Password, Algorithm) ->
|
|||
Config = raw_config(Algorithm),
|
||||
|
||||
{ok, _} = emqx:update_config(
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}),
|
||||
?PATH,
|
||||
{create_authenticator, ?GLOBAL, Config}
|
||||
),
|
||||
|
||||
{ok, [#{state := State}]} = emqx_authentication:list_authenticators(?GLOBAL),
|
||||
|
||||
emqx_enhanced_authn_scram_mnesia:add_user(
|
||||
#{user_id => Username, password => Password},
|
||||
State).
|
||||
#{user_id => Username, password => Password},
|
||||
State
|
||||
).
|
||||
|
||||
receive_packet() ->
|
||||
receive
|
||||
|
|
Loading…
Reference in New Issue