refactor(authn api): use config schemas for request validations (#5999)
This commit is contained in:
parent
d9cb0283f3
commit
800b4b32c7
|
@ -268,4 +268,3 @@ dir(ChainName, ID) when is_binary(ID) ->
|
||||||
binary:replace(iolist_to_binary([to_bin(ChainName), "-", ID]), <<":">>, <<"-">>);
|
binary:replace(iolist_to_binary([to_bin(ChainName), "-", ID]), <<":">>, <<"-">>);
|
||||||
dir(ChainName, Config) when is_map(Config) ->
|
dir(ChainName, Config) when is_map(Config) ->
|
||||||
dir(ChainName, authenticator_id(Config)).
|
dir(ChainName, authenticator_id(Config)).
|
||||||
|
|
||||||
|
|
|
@ -352,10 +352,13 @@ listener_id(Type, ListenerName) ->
|
||||||
list_to_atom(lists:append([str(Type), ":", str(ListenerName)])).
|
list_to_atom(lists:append([str(Type), ":", str(ListenerName)])).
|
||||||
|
|
||||||
parse_listener_id(Id) ->
|
parse_listener_id(Id) ->
|
||||||
[Type, Name] = string:split(str(Id), ":", leading),
|
case string:split(str(Id), ":", leading) of
|
||||||
case lists:member(Type, ?TYPES_STRING) of
|
[Type, Name] ->
|
||||||
true -> {list_to_existing_atom(Type), list_to_atom(Name)};
|
case lists:member(Type, ?TYPES_STRING) of
|
||||||
false -> {error, {invalid_listener_id, Id}}
|
true -> {list_to_existing_atom(Type), list_to_atom(Name)};
|
||||||
|
false -> {error, {invalid_listener_id, Id}}
|
||||||
|
end;
|
||||||
|
_ -> {error, {invalid_listener_id, Id}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
zone(Opts) ->
|
zone(Opts) ->
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -21,12 +21,11 @@
|
||||||
-export([ common_fields/0
|
-export([ common_fields/0
|
||||||
, roots/0
|
, roots/0
|
||||||
, fields/1
|
, fields/1
|
||||||
|
, authenticator_type/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% only for doc generation
|
%% only for doc generation
|
||||||
roots() -> [{authenticator_config,
|
roots() -> [{authenticator_config, hoconsc:mk(authenticator_type())}].
|
||||||
#{type => hoconsc:union(config_refs([Module || {_AuthnType, Module} <- emqx_authn:providers()]))
|
|
||||||
}}].
|
|
||||||
|
|
||||||
fields(_) -> [].
|
fields(_) -> [].
|
||||||
|
|
||||||
|
@ -38,5 +37,8 @@ enable(type) -> boolean();
|
||||||
enable(default) -> true;
|
enable(default) -> true;
|
||||||
enable(_) -> undefined.
|
enable(_) -> undefined.
|
||||||
|
|
||||||
|
authenticator_type() ->
|
||||||
|
hoconsc:union(config_refs([Module || {_AuthnType, Module} <- emqx_authn:providers()])).
|
||||||
|
|
||||||
config_refs(Modules) ->
|
config_refs(Modules) ->
|
||||||
lists:append([Module:refs() || Module <- Modules]).
|
lists:append([Module:refs() || Module <- Modules]).
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
||||||
-include("emqx_authz.hrl").
|
-include("emqx_authn.hrl").
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
@ -27,12 +27,27 @@
|
||||||
-define(API_VERSION, "v5").
|
-define(API_VERSION, "v5").
|
||||||
-define(BASE_PATH, "api").
|
-define(BASE_PATH, "api").
|
||||||
|
|
||||||
|
-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() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
groups() ->
|
groups() ->
|
||||||
[].
|
[].
|
||||||
|
|
||||||
|
init_per_testcase(_, Config) ->
|
||||||
|
delete_authenticators([authentication], ?GLOBAL),
|
||||||
|
delete_authenticators([listeners, tcp, default, authentication], ?TCP_DEFAULT),
|
||||||
|
{atomic, ok} = mria:clear_table(emqx_authn_mnesia),
|
||||||
|
Config.
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:start_apps([emqx_authn, emqx_dashboard], fun set_special_configs/1),
|
ok = emqx_common_test_helpers:start_apps([emqx_authn, emqx_dashboard], fun set_special_configs/1),
|
||||||
Config.
|
Config.
|
||||||
|
@ -55,10 +70,379 @@ set_special_configs(emqx_dashboard) ->
|
||||||
set_special_configs(_App) ->
|
set_special_configs(_App) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_create_http_authn(_) ->
|
%%------------------------------------------------------------------------------
|
||||||
{ok, 200, _} = request(post, uri(["authentication"]),
|
%% Tests
|
||||||
emqx_authn_test_lib:http_example()),
|
%%------------------------------------------------------------------------------
|
||||||
{ok, 200, _} = request(get, uri(["authentication"])).
|
|
||||||
|
t_invalid_listener(_) ->
|
||||||
|
{ok, 404, _} = request(get, uri(["listeners", "invalid", "authentication"])),
|
||||||
|
{ok, 404, _} = request(get, uri(["listeners", "in:valid", "authentication"])).
|
||||||
|
|
||||||
|
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 ++ ["authentication"]),
|
||||||
|
ValidConfig),
|
||||||
|
|
||||||
|
InvalidConfig = ValidConfig#{method => <<"delete">>},
|
||||||
|
{ok, 400, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication"]),
|
||||||
|
InvalidConfig),
|
||||||
|
|
||||||
|
?assertAuthenticatorsMatch(
|
||||||
|
[#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>}],
|
||||||
|
PathPrefix ++ ["authentication"]).
|
||||||
|
|
||||||
|
test_authenticator(PathPrefix) ->
|
||||||
|
ValidConfig0 = emqx_authn_test_lib:http_example(),
|
||||||
|
|
||||||
|
{ok, 200, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication"]),
|
||||||
|
ValidConfig0),
|
||||||
|
|
||||||
|
{ok, 200, _} = request(
|
||||||
|
get,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:http"])),
|
||||||
|
|
||||||
|
{ok, 404, _} = request(
|
||||||
|
get,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:redis"])),
|
||||||
|
|
||||||
|
|
||||||
|
{ok, 404, _} = request(
|
||||||
|
put,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database"]),
|
||||||
|
emqx_authn_test_lib:built_in_database_example()),
|
||||||
|
|
||||||
|
InvalidConfig0 = ValidConfig0#{method => <<"delete">>},
|
||||||
|
{ok, 400, _} = request(
|
||||||
|
put,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:http"]),
|
||||||
|
InvalidConfig0),
|
||||||
|
|
||||||
|
ValidConfig1 = ValidConfig0#{pool_size => 9},
|
||||||
|
{ok, 200, _} = request(
|
||||||
|
put,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:http"]),
|
||||||
|
ValidConfig1),
|
||||||
|
|
||||||
|
{ok, 404, _} = request(
|
||||||
|
delete,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:redis"])),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(
|
||||||
|
delete,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:http"])),
|
||||||
|
|
||||||
|
?assertAuthenticatorsMatch([], PathPrefix ++ ["authentication"]).
|
||||||
|
|
||||||
|
test_authenticator_users(PathPrefix) ->
|
||||||
|
{ok, 200, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication"]),
|
||||||
|
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,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
|
||||||
|
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, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
|
||||||
|
User)
|
||||||
|
end,
|
||||||
|
ValidUsers),
|
||||||
|
|
||||||
|
{ok, 200, Page1Data} =
|
||||||
|
request(
|
||||||
|
get,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]) ++ "?page=1&limit=2"),
|
||||||
|
|
||||||
|
Page1Users = response_data(Page1Data),
|
||||||
|
|
||||||
|
{ok, 200, Page2Data} =
|
||||||
|
request(
|
||||||
|
get,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]) ++ "?page=2&limit=2"),
|
||||||
|
|
||||||
|
Page2Users = response_data(Page2Data),
|
||||||
|
|
||||||
|
?assertEqual(2, length(Page1Users)),
|
||||||
|
?assertEqual(1, length(Page2Users)),
|
||||||
|
|
||||||
|
?assertEqual(
|
||||||
|
[<<"u1">>, <<"u2">>, <<"u3">>],
|
||||||
|
lists:usort([ UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])).
|
||||||
|
|
||||||
|
test_authenticator_user(PathPrefix) ->
|
||||||
|
{ok, 200, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication"]),
|
||||||
|
emqx_authn_test_lib:built_in_database_example()),
|
||||||
|
|
||||||
|
User = #{user_id => <<"u1">>, password => <<"p1">>},
|
||||||
|
{ok, 201, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
|
||||||
|
User),
|
||||||
|
|
||||||
|
{ok, 404, _} = request(
|
||||||
|
get,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u123"])),
|
||||||
|
|
||||||
|
{ok, 409, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
|
||||||
|
User),
|
||||||
|
|
||||||
|
{ok, 200, UserData} = request(
|
||||||
|
get,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "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,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u1"]),
|
||||||
|
UserUpdate)
|
||||||
|
end,
|
||||||
|
ValidUserUpdates),
|
||||||
|
|
||||||
|
InvalidUserUpdates = [
|
||||||
|
#{user_id => <<"u1">>, password => <<"p1">>},
|
||||||
|
#{is_superuser => true}],
|
||||||
|
|
||||||
|
lists:foreach(
|
||||||
|
fun(UserUpdate) ->
|
||||||
|
{ok, 400, _} = request(
|
||||||
|
put,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u1"]),
|
||||||
|
UserUpdate)
|
||||||
|
end,
|
||||||
|
InvalidUserUpdates),
|
||||||
|
|
||||||
|
{ok, 404, _} = request(
|
||||||
|
delete,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u123"])),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(
|
||||||
|
delete,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "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 ++ ["authentication"]),
|
||||||
|
Conf)
|
||||||
|
end,
|
||||||
|
AuthenticatorConfs),
|
||||||
|
|
||||||
|
?assertAuthenticatorsMatch(
|
||||||
|
[
|
||||||
|
#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
|
||||||
|
#{<<"mechanism">> := <<"jwt">>},
|
||||||
|
#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>}
|
||||||
|
],
|
||||||
|
PathPrefix ++ ["authentication"]),
|
||||||
|
|
||||||
|
% Invalid moves
|
||||||
|
|
||||||
|
{ok, 400, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "jwt", "move"]),
|
||||||
|
#{position => <<"up">>}),
|
||||||
|
|
||||||
|
{ok, 400, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "jwt", "move"]),
|
||||||
|
#{}),
|
||||||
|
|
||||||
|
{ok, 404, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "jwt", "move"]),
|
||||||
|
#{position => <<"before:invalid">>}),
|
||||||
|
|
||||||
|
{ok, 404, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "jwt", "move"]),
|
||||||
|
#{position => <<"before:password-based:redis">>}),
|
||||||
|
|
||||||
|
{ok, 404, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "jwt", "move"]),
|
||||||
|
#{position => <<"before:password-based:redis">>}),
|
||||||
|
|
||||||
|
% Valid moves
|
||||||
|
|
||||||
|
{ok, 204, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "jwt", "move"]),
|
||||||
|
#{position => <<"top">>}),
|
||||||
|
|
||||||
|
?assertAuthenticatorsMatch(
|
||||||
|
[
|
||||||
|
#{<<"mechanism">> := <<"jwt">>},
|
||||||
|
#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
|
||||||
|
#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>}
|
||||||
|
],
|
||||||
|
PathPrefix ++ ["authentication"]),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "jwt", "move"]),
|
||||||
|
#{position => <<"bottom">>}),
|
||||||
|
|
||||||
|
?assertAuthenticatorsMatch(
|
||||||
|
[
|
||||||
|
#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
|
||||||
|
#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>},
|
||||||
|
#{<<"mechanism">> := <<"jwt">>}
|
||||||
|
],
|
||||||
|
PathPrefix ++ ["authentication"]),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "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 ++ ["authentication"]).
|
||||||
|
|
||||||
|
test_authenticator_import_users(PathPrefix) ->
|
||||||
|
{ok, 200, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication"]),
|
||||||
|
emqx_authn_test_lib:built_in_database_example()),
|
||||||
|
|
||||||
|
{ok, 400, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
|
||||||
|
#{}),
|
||||||
|
|
||||||
|
{ok, 400, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
|
||||||
|
#{filename => <<"/etc/passwd">>}),
|
||||||
|
|
||||||
|
{ok, 400, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
|
||||||
|
#{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,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
|
||||||
|
#{filename => JSONFileName}),
|
||||||
|
|
||||||
|
{ok, 204, _} = request(
|
||||||
|
post,
|
||||||
|
uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
|
||||||
|
#{filename => CSVFileName}).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Helpers
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
delete_authenticators(Path, Chain) ->
|
||||||
|
case emqx_authentication:list_authenticators(Chain) of
|
||||||
|
{error, _} -> ok;
|
||||||
|
{ok, Authenticators} ->
|
||||||
|
lists:foreach(
|
||||||
|
fun(#{id := ID}) ->
|
||||||
|
emqx:update_config(
|
||||||
|
Path,
|
||||||
|
{delete_authenticator, Chain, ID},
|
||||||
|
#{rawconf_with_defaults => true})
|
||||||
|
end,
|
||||||
|
Authenticators)
|
||||||
|
end.
|
||||||
|
|
||||||
|
response_data(Response) ->
|
||||||
|
#{<<"data">> := Data} = jiffy:decode(Response, [return_maps]),
|
||||||
|
Data.
|
||||||
|
|
||||||
request(Method, Url) ->
|
request(Method, Url) ->
|
||||||
request(Method, Url, []).
|
request(Method, Url, []).
|
||||||
|
@ -83,10 +467,7 @@ request(Method, Url, Body) ->
|
||||||
|
|
||||||
uri() -> uri([]).
|
uri() -> uri([]).
|
||||||
uri(Parts) when is_list(Parts) ->
|
uri(Parts) when is_list(Parts) ->
|
||||||
NParts = [E || E <- Parts],
|
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | Parts]).
|
||||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
|
|
||||||
|
|
||||||
get_sources(Result) -> jsx:decode(Result).
|
|
||||||
|
|
||||||
auth_header() ->
|
auth_header() ->
|
||||||
Username = <<"admin">>,
|
Username = <<"admin">>,
|
||||||
|
@ -94,6 +475,5 @@ auth_header() ->
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
|
||||||
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
{"Authorization", "Bearer " ++ binary_to_list(Token)}.
|
||||||
|
|
||||||
to_json(Hocon) ->
|
to_json(Map) ->
|
||||||
{ok, Map} =hocon:binary(Hocon),
|
|
||||||
jiffy:encode(Map).
|
jiffy:encode(Map).
|
||||||
|
|
|
@ -19,20 +19,15 @@
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
||||||
|
authenticator_example(Id) ->
|
||||||
|
#{Id := #{value := Example}} = emqx_authn_api:authenticator_examples(),
|
||||||
|
Example.
|
||||||
|
|
||||||
http_example() ->
|
http_example() ->
|
||||||
"""
|
authenticator_example('password-based:http').
|
||||||
{
|
|
||||||
mechanism = \"password-based\"
|
built_in_database_example() ->
|
||||||
backend = http
|
authenticator_example('password-based:built-in-database').
|
||||||
method = post
|
|
||||||
url = \"http://127.0.0.2:8080\"
|
jwt_example() ->
|
||||||
headers = {\"content-type\" = \"application/json\"}
|
authenticator_example(jwt).
|
||||||
body = {username = \"${username}\",
|
|
||||||
password = \"${password}\"}
|
|
||||||
pool_size = 8
|
|
||||||
connect_timeout = 5000
|
|
||||||
request_timeout = 5000
|
|
||||||
enable_pipelining = true
|
|
||||||
ssl = {enable = false}
|
|
||||||
}
|
|
||||||
""".
|
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
namespace() -> "dashboard".
|
namespace() -> "dashboard".
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
|
||||||
paths() -> ["/login", "/logout", "/users",
|
paths() -> ["/login", "/logout", "/users",
|
||||||
"/users/:username", "/users/:username/change_pwd"].
|
"/users/:username", "/users/:username/change_pwd"].
|
||||||
|
|
|
@ -5,16 +5,16 @@
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([spec/1, spec/2]).
|
-export([spec/1, spec/2]).
|
||||||
-export([translate_req/2]).
|
|
||||||
-export([namespace/0, fields/1]).
|
-export([namespace/0, fields/1]).
|
||||||
|
-export([schema_with_example/2, schema_with_examples/2]).
|
||||||
-export([error_codes/1, error_codes/2]).
|
-export([error_codes/1, error_codes/2]).
|
||||||
-define(MAX_ROW_LIMIT, 100).
|
|
||||||
|
|
||||||
%% API
|
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-export([
|
||||||
-compile(nowarn_export_all).
|
parse_spec_ref/2,
|
||||||
|
components/1,
|
||||||
|
filter_check_request/2,
|
||||||
|
filter_check_request_and_translate_body/2]).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
|
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
|
||||||
|
@ -22,33 +22,39 @@
|
||||||
-define(DEFAULT_FIELDS, [example, allowReserved, style,
|
-define(DEFAULT_FIELDS, [example, allowReserved, style,
|
||||||
explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]).
|
explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]).
|
||||||
|
|
||||||
-define(DEFAULT_FILTER, #{filter => fun ?MODULE:translate_req/2}).
|
|
||||||
|
|
||||||
-define(INIT_SCHEMA, #{fields => #{}, translations => #{}, validations => [], namespace => undefined}).
|
-define(INIT_SCHEMA, #{fields => #{}, translations => #{}, validations => [], namespace => undefined}).
|
||||||
|
|
||||||
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
|
||||||
-define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>, ?TO_REF(namespace(_M_), _F_)])).
|
-define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>, ?TO_REF(namespace(_M_), _F_)])).
|
||||||
-define(TO_COMPONENTS_PARAM(_M_, _F_), iolist_to_binary([<<"#/components/parameters/">>, ?TO_REF(namespace(_M_), _F_)])).
|
-define(TO_COMPONENTS_PARAM(_M_, _F_), iolist_to_binary([<<"#/components/parameters/">>, ?TO_REF(namespace(_M_), _F_)])).
|
||||||
|
|
||||||
|
-define(MAX_ROW_LIMIT, 100).
|
||||||
|
|
||||||
|
-type(request() :: #{bindings => map(), query_string => map(), body => map()}).
|
||||||
|
-type(request_meta() :: #{module => module(), path => string(), method => atom()}).
|
||||||
|
|
||||||
|
-type(filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}).
|
||||||
|
-type(filter() :: fun((request(), request_meta()) -> filter_result())).
|
||||||
|
|
||||||
|
-type(spec_opts() :: #{check_schema => boolean() | filter(), translate_body => boolean()}).
|
||||||
|
|
||||||
|
-type(route_path() :: string() | binary()).
|
||||||
|
-type(route_methods() :: map()).
|
||||||
|
-type(route_handler() :: atom()).
|
||||||
|
-type(route_options() :: #{filter => filter() | undefined}).
|
||||||
|
|
||||||
|
-type(api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}).
|
||||||
|
-type(api_spec_component() :: map()).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% API
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
%% @equiv spec(Module, #{check_schema => false})
|
%% @equiv spec(Module, #{check_schema => false})
|
||||||
-spec(spec(module()) ->
|
-spec(spec(module()) -> {list(api_spec_entry()), list(api_spec_component())}).
|
||||||
{list({Path, Specs, OperationId, Options}), list(Component)} when
|
|
||||||
Path :: string()|binary(),
|
|
||||||
Specs :: map(),
|
|
||||||
OperationId :: atom(),
|
|
||||||
Options :: #{filter => fun((map(),
|
|
||||||
#{module => module(), path => string(), method => atom()}) -> map())},
|
|
||||||
Component :: map()).
|
|
||||||
spec(Module) -> spec(Module, #{check_schema => false}).
|
spec(Module) -> spec(Module, #{check_schema => false}).
|
||||||
|
|
||||||
-spec(spec(module(), #{check_schema => boolean()}) ->
|
-spec(spec(module(), spec_opts()) -> {list(api_spec_entry()), list(api_spec_component())}).
|
||||||
{list({Path, Specs, OperationId, Options}), list(Component)} when
|
|
||||||
Path :: string()|binary(),
|
|
||||||
Specs :: map(),
|
|
||||||
OperationId :: atom(),
|
|
||||||
Options :: #{filter => fun((map(),
|
|
||||||
#{module => module(), path => string(), method => atom()}) -> map())},
|
|
||||||
Component :: map()).
|
|
||||||
spec(Module, Options) ->
|
spec(Module, Options) ->
|
||||||
Paths = apply(Module, paths, []),
|
Paths = apply(Module, paths, []),
|
||||||
{ApiSpec, AllRefs} =
|
{ApiSpec, AllRefs} =
|
||||||
|
@ -60,26 +66,10 @@ spec(Module, Options) ->
|
||||||
end, {[], []}, Paths),
|
end, {[], []}, Paths),
|
||||||
{ApiSpec, components(lists:usort(AllRefs))}.
|
{ApiSpec, components(lists:usort(AllRefs))}.
|
||||||
|
|
||||||
-spec(translate_req(#{binding => list(), query_string => list(), body => map()},
|
-spec(namespace() -> hocon_schema:name()).
|
||||||
#{module => module(), path => string(), method => atom()}) ->
|
|
||||||
{ok, #{binding => list(), query_string => list(), body => map()}}|
|
|
||||||
{400, 'BAD_REQUEST', binary()}).
|
|
||||||
translate_req(Request, #{module := Module, path := Path, method := Method}) ->
|
|
||||||
#{Method := Spec} = apply(Module, schema, [Path]),
|
|
||||||
try
|
|
||||||
Params = maps:get(parameters, Spec, []),
|
|
||||||
Body = maps:get(requestBody, Spec, []),
|
|
||||||
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
|
||||||
NewBody = check_requestBody(Request, Body, Module, hoconsc:is_schema(Body)),
|
|
||||||
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
|
||||||
catch throw:Error ->
|
|
||||||
{_, [{validation_error, ValidErr}]} = Error,
|
|
||||||
#{path := Key, reason := Reason} = ValidErr,
|
|
||||||
{400, 'BAD_REQUEST', iolist_to_binary(io_lib:format("~ts : ~p", [Key, Reason]))}
|
|
||||||
end.
|
|
||||||
|
|
||||||
namespace() -> "public".
|
namespace() -> "public".
|
||||||
|
|
||||||
|
-spec(fields(hocon_schema:name()) -> hocon_schema:fields()).
|
||||||
fields(page) ->
|
fields(page) ->
|
||||||
Desc = <<"Page number of the results to fetch.">>,
|
Desc = <<"Page number of the results to fetch.">>,
|
||||||
Meta = #{in => query, desc => Desc, default => 1, example => 1},
|
Meta = #{in => query, desc => Desc, default => 1, example => 1},
|
||||||
|
@ -90,9 +80,19 @@ fields(limit) ->
|
||||||
Meta = #{in => query, desc => Desc, default => ?MAX_ROW_LIMIT, example => 50},
|
Meta = #{in => query, desc => Desc, default => ?MAX_ROW_LIMIT, example => 50},
|
||||||
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}].
|
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}].
|
||||||
|
|
||||||
|
-spec(schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema_map()).
|
||||||
|
schema_with_example(Type, Example) ->
|
||||||
|
hoconsc:mk(Type, #{examples => #{<<"example">> => Example}}).
|
||||||
|
|
||||||
|
-spec(schema_with_examples(hocon_schema:type(), map()) -> hocon_schema:field_schema_map()).
|
||||||
|
schema_with_examples(Type, Examples) ->
|
||||||
|
hoconsc:mk(Type, #{examples => #{<<"examples">> => Examples}}).
|
||||||
|
|
||||||
|
-spec(error_codes(list(atom())) -> hocon_schema:fields()).
|
||||||
error_codes(Codes) ->
|
error_codes(Codes) ->
|
||||||
error_codes(Codes, <<"Error code to troubleshoot problems.">>).
|
error_codes(Codes, <<"Error code to troubleshoot problems.">>).
|
||||||
|
|
||||||
|
-spec(error_codes(nonempty_list(atom()), binary()) -> hocon_schema:fields()).
|
||||||
error_codes(Codes = [_ | _], MsgExample) ->
|
error_codes(Codes = [_ | _], MsgExample) ->
|
||||||
[
|
[
|
||||||
{code, hoconsc:mk(hoconsc:enum(Codes))},
|
{code, hoconsc:mk(hoconsc:enum(Codes))},
|
||||||
|
@ -102,9 +102,45 @@ error_codes(Codes = [_ | _], MsgExample) ->
|
||||||
})}
|
})}
|
||||||
].
|
].
|
||||||
|
|
||||||
support_check_schema(#{check_schema := true}) -> ?DEFAULT_FILTER;
|
%%------------------------------------------------------------------------------
|
||||||
support_check_schema(#{check_schema := Func}) when is_function(Func, 2) -> #{filter => Func};
|
%% Private functions
|
||||||
support_check_schema(_) -> #{filter => undefined}.
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
filter_check_request_and_translate_body(Request, RequestMeta) ->
|
||||||
|
translate_req(Request, RequestMeta, fun check_and_translate/3).
|
||||||
|
|
||||||
|
filter_check_request(Request, RequestMeta) ->
|
||||||
|
translate_req(Request, RequestMeta, fun check_only/3).
|
||||||
|
|
||||||
|
translate_req(Request, #{module := Module, path := Path, method := Method}, CheckFun) ->
|
||||||
|
#{Method := Spec} = apply(Module, schema, [Path]),
|
||||||
|
try
|
||||||
|
Params = maps:get(parameters, Spec, []),
|
||||||
|
Body = maps:get(requestBody, Spec, []),
|
||||||
|
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
||||||
|
NewBody = check_requestBody(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
|
||||||
|
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
||||||
|
catch throw:Error ->
|
||||||
|
{_, [{validation_error, ValidErr}]} = Error,
|
||||||
|
#{path := Key, reason := Reason} = ValidErr,
|
||||||
|
{400, 'BAD_REQUEST', iolist_to_binary(io_lib:format("~ts : ~p", [Key, Reason]))}
|
||||||
|
end.
|
||||||
|
|
||||||
|
check_and_translate(Schema, Map, Opts) ->
|
||||||
|
hocon_schema:check_plain(Schema, Map, Opts).
|
||||||
|
|
||||||
|
check_only(Schema, Map, Opts) ->
|
||||||
|
_ = hocon_schema:check_plain(Schema, Map, Opts),
|
||||||
|
Map.
|
||||||
|
|
||||||
|
support_check_schema(#{check_schema := true, translate_body := true}) ->
|
||||||
|
#{filter => fun filter_check_request_and_translate_body/2};
|
||||||
|
support_check_schema(#{check_schema := true}) ->
|
||||||
|
#{filter => fun filter_check_request/2};
|
||||||
|
support_check_schema(#{check_schema := Filter}) when is_function(Filter, 2) ->
|
||||||
|
#{filter => Filter};
|
||||||
|
support_check_schema(_) ->
|
||||||
|
#{filter => undefined}.
|
||||||
|
|
||||||
parse_spec_ref(Module, Path) ->
|
parse_spec_ref(Module, Path) ->
|
||||||
Schema =
|
Schema =
|
||||||
|
@ -143,10 +179,10 @@ check_parameter([{Name, Type} | Spec], Bindings, QueryStr, Module, BindingsAcc,
|
||||||
query ->
|
query ->
|
||||||
NewQueryStr = hocon_schema:check_plain(Schema, QueryStr, #{override_env => false}),
|
NewQueryStr = hocon_schema:check_plain(Schema, QueryStr, #{override_env => false}),
|
||||||
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
|
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
|
||||||
check_parameter(Spec, Bindings, QueryStr, Module, BindingsAcc, NewQueryStrAcc)
|
check_parameter(Spec, Bindings, QueryStr, Module,BindingsAcc, NewQueryStrAcc)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_requestBody(#{body := Body}, Schema, Module, true) ->
|
check_requestBody(#{body := Body}, Schema, Module, CheckFun, true) ->
|
||||||
Type0 = hocon_schema:field_schema(Schema, type),
|
Type0 = hocon_schema:field_schema(Schema, type),
|
||||||
Type =
|
Type =
|
||||||
case Type0 of
|
case Type0 of
|
||||||
|
@ -154,7 +190,7 @@ check_requestBody(#{body := Body}, Schema, Module, true) ->
|
||||||
_ -> Type0
|
_ -> Type0
|
||||||
end,
|
end,
|
||||||
NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]},
|
NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]},
|
||||||
#{<<"root">> := NewBody} = hocon_schema:check_plain(NewSchema, #{<<"root">> => Body}, #{override_env => false}),
|
#{<<"root">> := NewBody} = CheckFun(NewSchema, #{<<"root">> => Body}, #{override_env => false}),
|
||||||
NewBody;
|
NewBody;
|
||||||
%% TODO not support nest object check yet, please use ref!
|
%% TODO not support nest object check yet, please use ref!
|
||||||
%% RequestBody = [ {per_page, mk(integer(), #{}},
|
%% RequestBody = [ {per_page, mk(integer(), #{}},
|
||||||
|
@ -163,10 +199,10 @@ check_requestBody(#{body := Body}, Schema, Module, true) ->
|
||||||
%% {good_nest_2, mk(ref(?MODULE, good_ref), #{})}
|
%% {good_nest_2, mk(ref(?MODULE, good_ref), #{})}
|
||||||
%% ]}
|
%% ]}
|
||||||
%% ]
|
%% ]
|
||||||
check_requestBody(#{body := Body}, Spec, _Module, false) ->
|
check_requestBody(#{body := Body}, Spec, _Module, CheckFun, false) ->
|
||||||
lists:foldl(fun({Name, Type}, Acc) ->
|
lists:foldl(fun({Name, Type}, Acc) ->
|
||||||
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
||||||
maps:merge(Acc, hocon_schema:check_plain(Schema, Body))
|
maps:merge(Acc, CheckFun(Schema, Body, #{}))
|
||||||
end, #{}, Spec).
|
end, #{}, Spec).
|
||||||
|
|
||||||
%% tags, description, summary, security, deprecated
|
%% tags, description, summary, security, deprecated
|
||||||
|
@ -244,14 +280,15 @@ trans_desc(Spec, Hocon) ->
|
||||||
|
|
||||||
requestBody([], _Module) -> {[], []};
|
requestBody([], _Module) -> {[], []};
|
||||||
requestBody(Schema, Module) ->
|
requestBody(Schema, Module) ->
|
||||||
{Props, Refs} =
|
{{Props, Refs}, Examples} =
|
||||||
case hoconsc:is_schema(Schema) of
|
case hoconsc:is_schema(Schema) of
|
||||||
true ->
|
true ->
|
||||||
HoconSchema = hocon_schema:field_schema(Schema, type),
|
HoconSchema = hocon_schema:field_schema(Schema, type),
|
||||||
hocon_schema_to_spec(HoconSchema, Module);
|
SchemaExamples = hocon_schema:field_schema(Schema, examples),
|
||||||
false -> parse_object(Schema, Module)
|
{hocon_schema_to_spec(HoconSchema, Module), SchemaExamples};
|
||||||
|
false -> {parse_object(Schema, Module), undefined}
|
||||||
end,
|
end,
|
||||||
{#{<<"content">> => #{<<"application/json">> => #{<<"schema">> => Props}}},
|
{#{<<"content">> => content(Props, Examples)},
|
||||||
Refs}.
|
Refs}.
|
||||||
|
|
||||||
responses(Responses, Module) ->
|
responses(Responses, Module) ->
|
||||||
|
@ -264,19 +301,20 @@ response(Status, ?REF(StructName), {Acc, RefsAcc, Module}) ->
|
||||||
response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module});
|
response(Status, ?R_REF(Module, StructName), {Acc, RefsAcc, Module});
|
||||||
response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module}) ->
|
response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module}) ->
|
||||||
{Spec, Refs} = hocon_schema_to_spec(RRef, Module),
|
{Spec, Refs} = hocon_schema_to_spec(RRef, Module),
|
||||||
Content = #{<<"application/json">> => #{<<"schema">> => Spec}},
|
Content = content(Spec),
|
||||||
{Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module};
|
{Acc#{integer_to_binary(Status) => #{<<"content">> => Content}}, Refs ++ RefsAcc, Module};
|
||||||
response(Status, Schema, {Acc, RefsAcc, Module}) ->
|
response(Status, Schema, {Acc, RefsAcc, Module}) ->
|
||||||
case hoconsc:is_schema(Schema) of
|
case hoconsc:is_schema(Schema) of
|
||||||
true ->
|
true ->
|
||||||
Hocon = hocon_schema:field_schema(Schema, type),
|
Hocon = hocon_schema:field_schema(Schema, type),
|
||||||
|
Examples = hocon_schema:field_schema(Schema, examples),
|
||||||
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
|
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
|
||||||
Init = trans_desc(#{}, Schema),
|
Init = trans_desc(#{}, Schema),
|
||||||
Content = #{<<"application/json">> => #{<<"schema">> => Spec}},
|
Content = content(Spec, Examples),
|
||||||
{Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}}, Refs ++ RefsAcc, Module};
|
{Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}}, Refs ++ RefsAcc, Module};
|
||||||
false ->
|
false ->
|
||||||
{Props, Refs} = parse_object(Schema, Module),
|
{Props, Refs} = parse_object(Schema, Module),
|
||||||
Content = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => Props}}},
|
Content = #{<<"content">> => content(Props)},
|
||||||
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module}
|
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -467,3 +505,11 @@ parse_object(Other, Module) ->
|
||||||
is_required(Hocon) ->
|
is_required(Hocon) ->
|
||||||
hocon_schema:field_schema(Hocon, required) =:= true orelse
|
hocon_schema:field_schema(Hocon, required) =:= true orelse
|
||||||
hocon_schema:field_schema(Hocon, nullable) =:= false.
|
hocon_schema:field_schema(Hocon, nullable) =:= false.
|
||||||
|
|
||||||
|
content(ApiSpec) ->
|
||||||
|
content(ApiSpec, undefined).
|
||||||
|
|
||||||
|
content(ApiSpec, undefined) ->
|
||||||
|
#{<<"application/json">> => #{<<"schema">> => ApiSpec}};
|
||||||
|
content(ApiSpec, Examples) when is_map(Examples) ->
|
||||||
|
#{<<"application/json">> => Examples#{<<"schema">> => ApiSpec}}.
|
||||||
|
|
|
@ -209,14 +209,32 @@ t_in_mix_trans_error(_Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_api_spec(_Config) ->
|
t_api_spec(_Config) ->
|
||||||
{Spec, _Components} = emqx_dashboard_swagger:spec(?MODULE),
|
{Spec0, _} = emqx_dashboard_swagger:spec(?MODULE),
|
||||||
Filter = fun(V, S) -> lists:all(fun({_, _, _, #{filter := Filter}}) -> Filter =:= V end, S) end,
|
assert_all_filters_equal(Spec0, undefined),
|
||||||
?assertEqual(true, Filter(undefined, Spec)),
|
|
||||||
{Spec1, _Components1} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}),
|
{Spec1, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}),
|
||||||
?assertEqual(true, Filter(fun emqx_dashboard_swagger:translate_req/2, Spec1)),
|
assert_all_filters_equal(Spec1, undefined),
|
||||||
{Spec2, _Components2} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => fun emqx_dashboard_swagger:translate_req/2}),
|
|
||||||
?assertEqual(true, Filter(fun emqx_dashboard_swagger:translate_req/2, Spec2)),
|
CustomFilter = fun(Request, _RequestMeta) -> {ok, Request} end,
|
||||||
ok.
|
{Spec2, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => CustomFilter}),
|
||||||
|
assert_all_filters_equal(Spec2, CustomFilter),
|
||||||
|
|
||||||
|
{Spec3, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}),
|
||||||
|
Path = "/test/in/:filter",
|
||||||
|
|
||||||
|
Filter = filter(Spec3, Path),
|
||||||
|
Bindings = #{filter => <<"created">>},
|
||||||
|
|
||||||
|
?assertMatch(
|
||||||
|
{ok, #{bindings := #{filter := created}}},
|
||||||
|
trans_parameters(Path, Bindings, #{}, Filter)).
|
||||||
|
|
||||||
|
assert_all_filters_equal(Spec, Filter) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun({_, _, _, #{filter := F}}) ->
|
||||||
|
?assertEqual(Filter, F)
|
||||||
|
end,
|
||||||
|
Spec).
|
||||||
|
|
||||||
validate(Path, ExpectParams) ->
|
validate(Path, ExpectParams) ->
|
||||||
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
||||||
|
@ -226,10 +244,17 @@ validate(Path, ExpectParams) ->
|
||||||
?assertEqual([], Refs),
|
?assertEqual([], Refs),
|
||||||
Spec.
|
Spec.
|
||||||
|
|
||||||
|
filter(ApiSpec, Path) ->
|
||||||
|
[Filter] = [F || {P, _, _, #{filter := F}} <- ApiSpec, P =:= Path],
|
||||||
|
Filter.
|
||||||
|
|
||||||
trans_parameters(Path, Bindings, QueryStr) ->
|
trans_parameters(Path, Bindings, QueryStr) ->
|
||||||
|
trans_parameters(Path, Bindings, QueryStr, fun emqx_dashboard_swagger:filter_check_request/2).
|
||||||
|
|
||||||
|
trans_parameters(Path, Bindings, QueryStr, Filter) ->
|
||||||
Meta = #{module => ?MODULE, method => post, path => Path},
|
Meta = #{module => ?MODULE, method => post, path => Path},
|
||||||
Request = #{bindings => Bindings, query_string => QueryStr, body => #{}},
|
Request = #{bindings => Bindings, query_string => QueryStr, body => #{}},
|
||||||
emqx_dashboard_swagger:translate_req(Request, Meta).
|
Filter(Request, Meta).
|
||||||
|
|
||||||
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
t_ref_array_with_key/1, t_ref_array_without_key/1
|
t_ref_array_with_key/1, t_ref_array_without_key/1
|
||||||
]).
|
]).
|
||||||
-export([
|
-export([
|
||||||
t_object_trans/1, t_nest_object_trans/1, t_local_ref_trans/1,
|
t_object_trans/1, t_object_notrans/1, t_nest_object_trans/1, t_local_ref_trans/1,
|
||||||
t_remote_ref_trans/1, t_nest_ref_trans/1,
|
t_remote_ref_trans/1, t_nest_ref_trans/1,
|
||||||
t_ref_array_with_key_trans/1, t_ref_array_without_key_trans/1,
|
t_ref_array_with_key_trans/1, t_ref_array_without_key_trans/1,
|
||||||
t_ref_trans_error/1, t_object_trans_error/1
|
t_ref_trans_error/1, t_object_trans_error/1
|
||||||
|
@ -32,7 +32,7 @@ groups() -> [
|
||||||
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]},
|
t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]},
|
||||||
{validation, [parallel],
|
{validation, [parallel],
|
||||||
[
|
[
|
||||||
t_object_trans, t_local_ref_trans, t_remote_ref_trans,
|
t_object_trans, t_object_notrans, t_local_ref_trans, t_remote_ref_trans,
|
||||||
t_ref_array_with_key_trans, t_ref_array_without_key_trans, t_nest_ref_trans,
|
t_ref_array_with_key_trans, t_ref_array_without_key_trans, t_nest_ref_trans,
|
||||||
t_ref_trans_error, t_object_trans_error
|
t_ref_trans_error, t_object_trans_error
|
||||||
%% t_nest_object_trans,
|
%% t_nest_object_trans,
|
||||||
|
@ -173,8 +173,29 @@ t_ref_array_without_key(_Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_api_spec(_Config) ->
|
t_api_spec(_Config) ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE),
|
{Spec0, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}),
|
||||||
ok.
|
Path = "/object",
|
||||||
|
Body = #{
|
||||||
|
<<"per_page">> => 1,
|
||||||
|
<<"timeout">> => <<"infinity">>,
|
||||||
|
<<"inner_ref">> => #{
|
||||||
|
<<"webhook-host">> => <<"127.0.0.1:80">>,
|
||||||
|
<<"log_dir">> => <<"var/log/test">>,
|
||||||
|
<<"tag">> => <<"god_tag">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Filter0 = filter(Spec0, Path),
|
||||||
|
?assertMatch(
|
||||||
|
{ok, #{body := ActualBody}},
|
||||||
|
trans_requestBody(Path, Body, Filter0)),
|
||||||
|
|
||||||
|
{Spec1, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}),
|
||||||
|
Filter1 = filter(Spec1, Path),
|
||||||
|
?assertMatch(
|
||||||
|
{ok, #{body := #{<<"timeout">> := infinity}}},
|
||||||
|
trans_requestBody(Path, Body, Filter1)).
|
||||||
|
|
||||||
|
|
||||||
t_object_trans(_Config) ->
|
t_object_trans(_Config) ->
|
||||||
Path = "/object",
|
Path = "/object",
|
||||||
|
@ -205,6 +226,21 @@ t_object_trans(_Config) ->
|
||||||
?assertEqual(Expect, ActualBody),
|
?assertEqual(Expect, ActualBody),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_object_notrans(_Config) ->
|
||||||
|
Path = "/object",
|
||||||
|
Body = #{
|
||||||
|
<<"per_page">> => 1,
|
||||||
|
<<"timeout">> => <<"infinity">>,
|
||||||
|
<<"inner_ref">> => #{
|
||||||
|
<<"webhook-host">> => <<"127.0.0.1:80">>,
|
||||||
|
<<"log_dir">> => <<"var/log/test">>,
|
||||||
|
<<"tag">> => <<"god_tag">>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ok, #{body := ActualBody}} = trans_requestBody(Path, Body, fun emqx_dashboard_swagger:filter_check_request/2),
|
||||||
|
?assertEqual(Body, ActualBody),
|
||||||
|
ok.
|
||||||
|
|
||||||
t_nest_object_trans(_Config) ->
|
t_nest_object_trans(_Config) ->
|
||||||
Path = "/nest/object",
|
Path = "/nest/object",
|
||||||
Body = #{
|
Body = #{
|
||||||
|
@ -337,6 +373,7 @@ t_ref_array_with_key_trans(_Config) ->
|
||||||
{ok, NewRequest} = trans_requestBody(Path, Body),
|
{ok, NewRequest} = trans_requestBody(Path, Body),
|
||||||
?assertEqual(Expect, NewRequest),
|
?assertEqual(Expect, NewRequest),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_ref_array_without_key_trans(_Config) ->
|
t_ref_array_without_key_trans(_Config) ->
|
||||||
Path = "/ref/array/without/key",
|
Path = "/ref/array/without/key",
|
||||||
Body = [#{
|
Body = [#{
|
||||||
|
@ -401,10 +438,18 @@ validate(Path, ExpectSpec, ExpectRefs) ->
|
||||||
?assertEqual(ExpectRefs, Refs),
|
?assertEqual(ExpectRefs, Refs),
|
||||||
{Spec, emqx_dashboard_swagger:components(Refs)}.
|
{Spec, emqx_dashboard_swagger:components(Refs)}.
|
||||||
|
|
||||||
|
|
||||||
|
filter(ApiSpec, Path) ->
|
||||||
|
[Filter] = [F || {P, _, _, #{filter := F}} <- ApiSpec, P =:= Path],
|
||||||
|
Filter.
|
||||||
|
|
||||||
trans_requestBody(Path, Body) ->
|
trans_requestBody(Path, Body) ->
|
||||||
|
trans_requestBody(Path, Body, fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2).
|
||||||
|
|
||||||
|
trans_requestBody(Path, Body, Filter) ->
|
||||||
Meta = #{module => ?MODULE, method => post, path => Path},
|
Meta = #{module => ?MODULE, method => post, path => Path},
|
||||||
Request = #{bindings => #{}, query_string => #{}, body => Body},
|
Request = #{bindings => #{}, query_string => #{}, body => Body},
|
||||||
emqx_dashboard_swagger:translate_req(Request, Meta).
|
Filter(Request, Meta).
|
||||||
|
|
||||||
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
||||||
paths() ->
|
paths() ->
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
]).
|
]).
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
["/mqtt/topic_rewrite"].
|
["/mqtt/topic_rewrite"].
|
||||||
|
|
Loading…
Reference in New Issue