519 lines
14 KiB
Erlang
519 lines
14 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2021-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
%%
|
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
|
%% you may not use this file except in compliance with the License.
|
|
%% You may obtain a copy of the License at
|
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
|
%%
|
|
%% Unless required by applicable law or agreed to in writing, software
|
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
%% See the License for the specific language governing permissions and
|
|
%% limitations under the License.
|
|
%%--------------------------------------------------------------------
|
|
|
|
-module(emqx_authn_api_SUITE).
|
|
|
|
-compile(nowarn_export_all).
|
|
-compile(export_all).
|
|
|
|
-import(emqx_dashboard_api_test_helpers, [request/3, uri/1]).
|
|
|
|
-include("emqx_authn.hrl").
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include_lib("common_test/include/ct.hrl").
|
|
|
|
-define(TCP_DEFAULT, 'tcp:default').
|
|
|
|
-define(assertAuthenticatorsMatch(Guard, Path),
|
|
(fun() ->
|
|
{ok, 200, Response} = request(get, uri(Path)),
|
|
?assertMatch(Guard, jiffy:decode(Response, [return_maps]))
|
|
end)()
|
|
).
|
|
|
|
all() ->
|
|
emqx_common_test_helpers:all(?MODULE).
|
|
|
|
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
|
|
),
|
|
|
|
emqx_authn_test_lib:delete_authenticators(
|
|
[listeners, tcp, default, ?CONF_NS_ATOM],
|
|
?TCP_DEFAULT
|
|
),
|
|
|
|
{atomic, ok} = mria:clear_table(emqx_authn_mnesia),
|
|
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
|
|
),
|
|
|
|
?AUTHN:delete_chain(?GLOBAL),
|
|
{ok, Chains} = ?AUTHN:list_chains(),
|
|
?assertEqual(length(Chains), 0),
|
|
Config.
|
|
|
|
end_per_suite(_Config) ->
|
|
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authn]),
|
|
ok.
|
|
|
|
set_special_configs(emqx_dashboard) ->
|
|
emqx_dashboard_api_test_helpers:set_default_config();
|
|
set_special_configs(_App) ->
|
|
ok.
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% Tests
|
|
%%------------------------------------------------------------------------------
|
|
|
|
t_invalid_listener(_) ->
|
|
{ok, 404, _} = request(get, uri(["listeners", "invalid", ?CONF_NS])),
|
|
{ok, 404, _} = request(get, uri(["listeners", "in:valid", ?CONF_NS])).
|
|
|
|
t_authenticators(_) ->
|
|
test_authenticators([]).
|
|
|
|
t_authenticator(_) ->
|
|
test_authenticator([]).
|
|
|
|
t_authenticator_users(_) ->
|
|
test_authenticator_users([]).
|
|
|
|
t_authenticator_user(_) ->
|
|
test_authenticator_user([]).
|
|
|
|
t_authenticator_move(_) ->
|
|
test_authenticator_move([]).
|
|
|
|
t_authenticator_import_users(_) ->
|
|
test_authenticator_import_users([]).
|
|
|
|
t_listener_authenticators(_) ->
|
|
test_authenticators(["listeners", ?TCP_DEFAULT]).
|
|
|
|
t_listener_authenticator(_) ->
|
|
test_authenticator(["listeners", ?TCP_DEFAULT]).
|
|
|
|
t_listener_authenticator_users(_) ->
|
|
test_authenticator_users(["listeners", ?TCP_DEFAULT]).
|
|
|
|
t_listener_authenticator_user(_) ->
|
|
test_authenticator_user(["listeners", ?TCP_DEFAULT]).
|
|
|
|
t_listener_authenticator_move(_) ->
|
|
test_authenticator_move(["listeners", ?TCP_DEFAULT]).
|
|
|
|
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
|
|
),
|
|
|
|
{ok, 409, _} = request(
|
|
post,
|
|
uri(PathPrefix ++ [?CONF_NS]),
|
|
ValidConfig
|
|
),
|
|
|
|
InvalidConfig0 = ValidConfig#{method => <<"delete">>},
|
|
{ok, 400, _} = request(
|
|
post,
|
|
uri(PathPrefix ++ [?CONF_NS]),
|
|
InvalidConfig0
|
|
),
|
|
|
|
InvalidConfig1 = ValidConfig#{
|
|
method => <<"get">>,
|
|
headers => #{<<"content-type">> => <<"application/json">>}
|
|
},
|
|
{ok, 400, _} = request(
|
|
post,
|
|
uri(PathPrefix ++ [?CONF_NS]),
|
|
InvalidConfig1
|
|
),
|
|
|
|
?assertAuthenticatorsMatch(
|
|
[#{<<"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
|
|
),
|
|
{ok, 200, _} = request(
|
|
get,
|
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])
|
|
),
|
|
|
|
{ok, 200, Res} = request(
|
|
get,
|
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])
|
|
),
|
|
{ok, RList} = emqx_json:safe_decode(Res),
|
|
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,
|
|
lists:map(EqualFun, MetricsList),
|
|
?assertEqual(
|
|
<<"connected">>,
|
|
LookFun([<<"status">>])
|
|
),
|
|
{ok, 404, _} = request(
|
|
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()
|
|
),
|
|
|
|
InvalidConfig0 = ValidConfig0#{method => <<"delete">>},
|
|
{ok, 400, _} = request(
|
|
put,
|
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
|
InvalidConfig0
|
|
),
|
|
|
|
InvalidConfig1 = ValidConfig0#{
|
|
method => <<"get">>,
|
|
headers => #{<<"content-type">> => <<"application/json">>}
|
|
},
|
|
{ok, 400, _} = request(
|
|
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
|
|
),
|
|
|
|
{ok, 404, _} = request(
|
|
delete,
|
|
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
|
|
),
|
|
|
|
{ok, 204, _} = request(
|
|
delete,
|
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])
|
|
),
|
|
|
|
?assertAuthenticatorsMatch([], PathPrefix ++ [?CONF_NS]).
|
|
|
|
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()
|
|
),
|
|
|
|
InvalidUsers = [
|
|
#{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
|
|
),
|
|
|
|
ValidUsers = [
|
|
#{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
|
|
),
|
|
|
|
{ok, 200, Page1Data} = request(get, UsersUri ++ "?page=1&limit=2"),
|
|
|
|
#{
|
|
<<"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]),
|
|
|
|
?assertEqual(2, length(Page1Users)),
|
|
?assertEqual(1, length(Page2Users)),
|
|
|
|
?assertEqual(
|
|
[<<"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()
|
|
),
|
|
|
|
User = #{user_id => <<"u1">>, password => <<"p1">>},
|
|
{ok, 201, _} = request(post, UsersUri, User),
|
|
|
|
{ok, 404, _} = request(get, UsersUri ++ "/u123"),
|
|
|
|
{ok, 409, _} = request(post, UsersUri, User),
|
|
|
|
{ok, 200, UserData} = request(get, UsersUri ++ "/u1"),
|
|
|
|
FetchedUser = jiffy:decode(UserData, [return_maps]),
|
|
?assertMatch(#{<<"user_id">> := <<"u1">>}, FetchedUser),
|
|
?assertNotMatch(#{<<"password">> := _}, FetchedUser),
|
|
|
|
ValidUserUpdates = [
|
|
#{password => <<"p1">>},
|
|
#{password => <<"p1">>, is_superuser => true}
|
|
],
|
|
|
|
lists:foreach(
|
|
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
|
|
),
|
|
|
|
{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()
|
|
],
|
|
|
|
lists:foreach(
|
|
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]
|
|
),
|
|
|
|
%% Invalid moves
|
|
|
|
{ok, 400, _} = request(
|
|
post,
|
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
|
#{position => <<"up">>}
|
|
),
|
|
|
|
{ok, 400, _} = request(
|
|
post,
|
|
uri(PathPrefix ++ [?CONF_NS, "jwt", "move"]),
|
|
#{}
|
|
),
|
|
|
|
{ok, 404, _} = request(
|
|
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">>}
|
|
),
|
|
|
|
{ok, 404, _} = request(
|
|
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">>}
|
|
),
|
|
|
|
?assertAuthenticatorsMatch(
|
|
[
|
|
#{<<"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">>}
|
|
),
|
|
|
|
?assertAuthenticatorsMatch(
|
|
[
|
|
#{<<"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">>}
|
|
),
|
|
|
|
?assertAuthenticatorsMatch(
|
|
[
|
|
#{<<"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">>}
|
|
),
|
|
|
|
?assertAuthenticatorsMatch(
|
|
[
|
|
#{<<"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"]
|
|
),
|
|
|
|
{ok, 200, _} = request(
|
|
post,
|
|
uri(PathPrefix ++ [?CONF_NS]),
|
|
emqx_authn_test_lib:built_in_database_example()
|
|
),
|
|
|
|
{ok, 400, _} = request(post, ImportUri, #{}),
|
|
|
|
{ok, 400, _} = request(post, ImportUri, #{filename => <<"/etc/passwd">>}),
|
|
|
|
{ok, 400, _} = request(post, ImportUri, #{filename => <<"/not_exists.csv">>}),
|
|
|
|
Dir = code:lib_dir(emqx_authn, test),
|
|
JSONFileName = filename:join([Dir, <<"data/user-credentials.json">>]),
|
|
CSVFileName = filename:join([Dir, <<"data/user-credentials.csv">>]),
|
|
|
|
{ok, 204, _} = request(post, ImportUri, #{filename => JSONFileName}),
|
|
|
|
{ok, 204, _} = request(post, ImportUri, #{filename => CSVFileName}).
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% Helpers
|
|
%%------------------------------------------------------------------------------
|
|
|
|
request(Method, Url) ->
|
|
request(Method, Url, []).
|