style: erlfmt apps/emqx_authn

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

View File

@ -1,23 +1,32 @@
%% -*- mode: erlang -*-
{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]}.

View File

@ -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/"}]}
]}.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]}
].

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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