Merge branch 'release-57' into 0617-release-57-sync

* release-57:
  chore(auth,http): cache REs for parsing URIs
  fix(auth,http): improve URI handling
  chore: revert ULOG/ELOG
  test: generate dispatch.eterm in dashboard test
  docs: refine change log
  feat: make the dashboard restart quicker
  chore: fix typo
  fix(http authz): handle unknown content types in responses
  chore: change types of mysql and mongodb fields to `template()`
  fix(client mgmt api): allow projecting `client_attrs` from client fields
  fix(emqx_rule_funcs): expose regex_extract function to rule engine
This commit is contained in:
Ilya Averyanov 2024-06-17 18:17:33 +03:00
commit f8e6aab86f
27 changed files with 340 additions and 107 deletions

View File

@ -1,3 +1,5 @@
简体中文 | [English](./README.md) | [Русский](./README-RU.md)
# EMQX # EMQX
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases) [![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases)

View File

@ -1,3 +1,5 @@
Русский | [简体中文](./README-CN.md) | [English](./README.md)
# Брокер EMQX # Брокер EMQX
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases) [![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases)

View File

@ -1,3 +1,5 @@
English | [简体中文](./README-CN.md) | [Русский](./README-RU.md)
# EMQX # EMQX
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases) [![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases)

View File

@ -119,12 +119,12 @@
%% All Messages received %% All Messages received
{counter, 'messages.received', << {counter, 'messages.received', <<
"Number of messages received from the client, equal to the sum of " "Number of messages received from the client, equal to the sum of "
"messages.qos0.received\fmessages.qos1.received and messages.qos2.received" "messages.qos0.received, messages.qos1.received and messages.qos2.received"
>>}, >>},
%% All Messages sent %% All Messages sent
{counter, 'messages.sent', << {counter, 'messages.sent', <<
"Number of messages sent to the client, equal to the sum of " "Number of messages sent to the client, equal to the sum of "
"messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent" "messages.qos0.sent, messages.qos1.sent and messages.qos2.sent"
>>}, >>},
%% QoS0 Messages received %% QoS0 Messages received
{counter, 'messages.qos0.received', <<"Number of QoS 0 messages received from clients">>}, {counter, 'messages.qos0.received', <<"Number of QoS 0 messages received from clients">>},

View File

@ -100,7 +100,7 @@ update_config(Path, ConfigRequest) ->
override_to => cluster override_to => cluster
}). }).
-spec parse_http_resp_body(binary(), binary()) -> allow | deny | ignore | error. -spec parse_http_resp_body(binary(), binary()) -> allow | deny | ignore | error | {error, term()}.
parse_http_resp_body(<<"application/x-www-form-urlencoded", _/binary>>, Body) -> parse_http_resp_body(<<"application/x-www-form-urlencoded", _/binary>>, Body) ->
try try
result(maps:from_list(cow_qs:parse_qs(Body))) result(maps:from_list(cow_qs:parse_qs(Body)))
@ -112,7 +112,9 @@ parse_http_resp_body(<<"application/json", _/binary>>, Body) ->
result(emqx_utils_json:decode(Body, [return_maps])) result(emqx_utils_json:decode(Body, [return_maps]))
catch catch
_:_ -> error _:_ -> error
end. end;
parse_http_resp_body(ContentType = <<_/binary>>, _Body) ->
{error, <<"unsupported content-type: ", ContentType/binary>>}.
result(#{<<"result">> := <<"allow">>}) -> allow; result(#{<<"result">> := <<"allow">>}) -> allow;
result(#{<<"result">> := <<"deny">>}) -> deny; result(#{<<"result">> := <<"deny">>}) -> deny;

View File

@ -106,6 +106,9 @@ authorize(
body => Body body => Body
}), }),
nomatch; nomatch;
{error, Reason} ->
?tp(error, bad_authz_http_response, #{reason => Reason}),
nomatch;
Result -> Result ->
{matched, Result} {matched, Result}
end; end;

View File

@ -463,6 +463,72 @@ t_placeholder_and_body(_Config) ->
emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
). ).
%% Checks that we don't crash when receiving an unsupported content-type back.
t_bad_response_content_type(_Config) ->
ok = setup_handler_and_config(
fun(Req0, State) ->
?assertEqual(
<<"/authz/users/">>,
cowboy_req:path(Req0)
),
{ok, _PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0),
Req = cowboy_req:reply(
200,
#{<<"content-type">> => <<"text/csv">>},
"hi",
Req1
),
{ok, Req, State}
end,
#{
<<"method">> => <<"post">>,
<<"body">> => #{
<<"username">> => <<"${username}">>,
<<"clientid">> => <<"${clientid}">>,
<<"peerhost">> => <<"${peerhost}">>,
<<"proto_name">> => <<"${proto_name}">>,
<<"mountpoint">> => <<"${mountpoint}">>,
<<"topic">> => <<"${topic}">>,
<<"action">> => <<"${action}">>,
<<"access">> => <<"${access}">>,
<<"CN">> => ?PH_CERT_CN_NAME,
<<"CS">> => ?PH_CERT_SUBJECT
},
<<"headers">> => #{
<<"accept">> => <<"text/plain">>,
<<"content-type">> => <<"application/json">>
}
}
),
ClientInfo = #{
clientid => <<"client id">>,
username => <<"user name">>,
peerhost => {127, 0, 0, 1},
protocol => <<"MQTT">>,
mountpoint => <<"MOUNTPOINT">>,
zone => default,
listener => {tcp, default},
cn => ?PH_CERT_CN_NAME,
dn => ?PH_CERT_SUBJECT
},
?check_trace(
?assertEqual(
deny,
emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
),
fun(Trace) ->
?assertMatch(
[#{reason := <<"unsupported content-type", _/binary>>}],
?of_kind(bad_authz_http_response, Trace)
),
ok
end
).
t_no_value_for_placeholder(_Config) -> t_no_value_for_placeholder(_Config) ->
ok = setup_handler_and_config( ok = setup_handler_and_config(
fun(Req0, State) -> fun(Req0, State) ->

View File

@ -32,6 +32,7 @@
%% Swagger specs from hocon schema %% Swagger specs from hocon schema
-export([ -export([
api_spec/0, api_spec/0,
check_api_schema/2,
paths/0, paths/0,
schema/1, schema/1,
namespace/0 namespace/0
@ -96,7 +97,7 @@
namespace() -> "actions_and_sources". namespace() -> "actions_and_sources".
api_spec() -> api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => fun check_api_schema/2}). emqx_dashboard_swagger:spec(?MODULE, #{check_schema => fun ?MODULE:check_api_schema/2}).
paths() -> paths() ->
[ [

View File

@ -1,6 +1,6 @@
{application, emqx_bridge_mongodb, [ {application, emqx_bridge_mongodb, [
{description, "EMQX Enterprise MongoDB Bridge"}, {description, "EMQX Enterprise MongoDB Bridge"},
{vsn, "0.3.0"}, {vsn, "0.3.1"},
{registered, []}, {registered, []},
{applications, [ {applications, [
kernel, kernel,

View File

@ -100,8 +100,10 @@ fields(mongodb_action) ->
); );
fields(action_parameters) -> fields(action_parameters) ->
[ [
{collection, mk(binary(), #{desc => ?DESC("collection"), default => <<"mqtt">>})}, {collection,
{payload_template, mk(binary(), #{required => false, desc => ?DESC("payload_template")})} mk(emqx_schema:template(), #{desc => ?DESC("collection"), default => <<"mqtt">>})},
{payload_template,
mk(emqx_schema:template(), #{required => false, desc => ?DESC("payload_template")})}
]; ];
fields(connector_resource_opts) -> fields(connector_resource_opts) ->
emqx_connector_schema:resource_opts_fields(); emqx_connector_schema:resource_opts_fields();

View File

@ -1,6 +1,6 @@
{application, emqx_bridge_mysql, [ {application, emqx_bridge_mysql, [
{description, "EMQX Enterprise MySQL Bridge"}, {description, "EMQX Enterprise MySQL Bridge"},
{vsn, "0.1.5"}, {vsn, "0.1.6"},
{registered, []}, {registered, []},
{applications, [ {applications, [
kernel, kernel,

View File

@ -146,7 +146,7 @@ fields(action_parameters) ->
[ [
{sql, {sql,
mk( mk(
binary(), emqx_schema:template(),
#{desc => ?DESC("sql_template"), default => ?DEFAULT_SQL, format => <<"sql">>} #{desc => ?DESC("sql_template"), default => ?DEFAULT_SQL, format => <<"sql">>}
)} )}
]; ];

View File

@ -144,17 +144,27 @@ reset(Node, KeyPath, Opts) ->
%% @doc Called from build script. %% @doc Called from build script.
%% TODO: move to a external escript after all refactoring is done %% TODO: move to a external escript after all refactoring is done
dump_schema(Dir, SchemaModule) -> dump_schema(Dir, SchemaModule) ->
%% TODO: Load all apps instead of only emqx_dashboard %% Load all apps in ERL_LIBS
%% as this will help schemas that searches for apps with %% as this will help schemas that searches for apps with
%% relevant schema definitions %% relevant schema definitions
_ = application:load(emqx_dashboard), lists:foreach(
fun(LibPath) ->
Lib = list_to_atom(lists:last(filename:split(LibPath))),
load(SchemaModule, Lib)
end,
string:lexemes(os:getenv("ERL_LIBS"), ":;")
),
ok = emqx_dashboard_desc_cache:init(), ok = emqx_dashboard_desc_cache:init(),
lists:foreach( lists:foreach(
fun(Lang) -> fun(Lang) ->
ok = gen_schema_json(Dir, SchemaModule, Lang) ok = gen_schema_json(Dir, SchemaModule, Lang)
end, end,
["en", "zh"] ["en", "zh"]
). ),
emqx_dashboard:save_dispatch_eterm(SchemaModule).
load(emqx_enterprise_schema, emqx_telemetry) -> ignore;
load(_, Lib) -> ok = application:load(Lib).
%% for scripts/spellcheck. %% for scripts/spellcheck.
gen_schema_json(Dir, SchemaModule, Lang) -> gen_schema_json(Dir, SchemaModule, Lang) ->

View File

@ -28,11 +28,15 @@
%% Authorization %% Authorization
-export([authorize/1]). -export([authorize/1]).
-export([save_dispatch_eterm/1]).
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/http_api.hrl"). -include_lib("emqx/include/http_api.hrl").
-include_lib("emqx/include/emqx_release.hrl"). -include_lib("emqx/include/emqx_release.hrl").
-dialyzer({[no_opaque, no_match, no_return], [init_cache_dispatch/2, start_listeners/1]}).
-define(EMQX_MIDDLE, emqx_dashboard_middleware). -define(EMQX_MIDDLE, emqx_dashboard_middleware).
-define(DISPATCH_FILE, "dispatch.eterm").
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Start/Stop Listeners %% Start/Stop Listeners
@ -46,6 +50,42 @@ stop_listeners() ->
start_listeners(Listeners) -> start_listeners(Listeners) ->
{ok, _} = application:ensure_all_started(minirest), {ok, _} = application:ensure_all_started(minirest),
SwaggerSupport = emqx:get_config([dashboard, swagger_support], true),
InitDispatch = dispatch(),
{OkListeners, ErrListeners} =
lists:foldl(
fun({Name, Protocol, Bind, RanchOptions, ProtoOpts}, {OkAcc, ErrAcc}) ->
ok = init_cache_dispatch(Name, InitDispatch),
Options = #{
dispatch => InitDispatch,
swagger_support => SwaggerSupport,
protocol => Protocol,
protocol_options => ProtoOpts
},
Minirest = minirest_option(Options),
case minirest:start(Name, RanchOptions, Minirest) of
{ok, _} ->
?ULOG("Listener ~ts on ~ts started.~n", [
Name, emqx_listeners:format_bind(Bind)
]),
{[Name | OkAcc], ErrAcc};
{error, _Reason} ->
%% Don't record the reason because minirest already does(too much logs noise).
{OkAcc, [Name | ErrAcc]}
end
end,
{[], []},
listeners(ensure_ssl_cert(Listeners))
),
case ErrListeners of
[] ->
optvar:set(emqx_dashboard_listeners_ready, OkListeners),
ok;
_ ->
{error, ErrListeners}
end.
minirest_option(Options) ->
Authorization = {?MODULE, authorize}, Authorization = {?MODULE, authorize},
GlobalSpec = #{ GlobalSpec = #{
openapi => "3.0.0", openapi => "3.0.0",
@ -68,42 +108,33 @@ start_listeners(Listeners) ->
} }
} }
}, },
BaseMinirest = #{ Base =
base_path => emqx_dashboard_swagger:base_path(), #{
modules => minirest_api:find_api_modules(apps()), base_path => emqx_dashboard_swagger:base_path(),
authorization => Authorization, modules => minirest_api:find_api_modules(apps()),
log => audit_log_fun(), authorization => Authorization,
security => [#{'basicAuth' => []}, #{'bearerAuth' => []}], log => audit_log_fun(),
swagger_global_spec => GlobalSpec, security => [#{'basicAuth' => []}, #{'bearerAuth' => []}],
dispatch => dispatch(), swagger_global_spec => GlobalSpec,
middlewares => [?EMQX_MIDDLE, cowboy_router, cowboy_handler], dispatch => static_dispatch(),
swagger_support => emqx:get_config([dashboard, swagger_support], true) middlewares => [?EMQX_MIDDLE, cowboy_router, cowboy_handler],
}, swagger_support => true
{OkListeners, ErrListeners} = },
lists:foldl( maps:merge(Base, Options).
fun({Name, Protocol, Bind, RanchOptions, ProtoOpts}, {OkAcc, ErrAcc}) ->
Minirest = BaseMinirest#{protocol => Protocol, protocol_options => ProtoOpts}, %% save dispatch to priv dir.
case minirest:start(Name, RanchOptions, Minirest) of save_dispatch_eterm(SchemaMod) ->
{ok, _} -> Dir = code:priv_dir(emqx_dashboard),
?ULOG("Listener ~ts on ~ts started.~n", [ emqx_config:put([dashboard], #{i18n_lang => en, swagger_support => false}),
Name, emqx_listeners:format_bind(Bind) os:putenv("SCHEMA_MOD", atom_to_list(SchemaMod)),
]), DispatchFile = filename:join([Dir, ?DISPATCH_FILE]),
{[Name | OkAcc], ErrAcc}; io:format(user, "===< Generating: ~s~n", [DispatchFile]),
{error, _Reason} -> #{dispatch := Dispatch} = generate_dispatch(),
%% Don't record the reason because minirest already does(too much logs noise). IoData = io_lib:format("~p.~n", [Dispatch]),
{OkAcc, [Name | ErrAcc]} ok = file:write_file(DispatchFile, IoData),
end {ok, [SaveDispatch]} = file:consult(DispatchFile),
end, SaveDispatch =/= Dispatch andalso erlang:error("bad dashboard dispatch.eterm file generated"),
{[], []}, ok.
listeners(ensure_ssl_cert(Listeners))
),
case ErrListeners of
[] ->
optvar:set(emqx_dashboard_listeners_ready, OkListeners),
ok;
_ ->
{error, ErrListeners}
end.
stop_listeners(Listeners) -> stop_listeners(Listeners) ->
optvar:unset(emqx_dashboard_listeners_ready), optvar:unset(emqx_dashboard_listeners_ready),
@ -127,6 +158,34 @@ wait_for_listeners() ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% internal %% internal
%%--------------------------------------------------------------------
init_cache_dispatch(Name, Dispatch0) ->
Dispatch1 = [{_, _, Rules}] = trails:single_host_compile(Dispatch0),
FileName = filename:join(code:priv_dir(emqx_dashboard), ?DISPATCH_FILE),
Dispatch2 =
case file:consult(FileName) of
{ok, [[{Host, Path, CacheRules}]]} ->
Trails = trails:trails([{cowboy_swagger_handler, #{server => 'http:dashboard'}}]),
[{_, _, SwaggerRules}] = trails:single_host_compile(Trails),
[{Host, Path, CacheRules ++ SwaggerRules ++ Rules}];
{error, _} ->
Dispatch1
end,
persistent_term:put(Name, Dispatch2).
generate_dispatch() ->
Options = #{
dispatch => [],
swagger_support => false,
protocol => http,
protocol_options => proto_opts(#{})
},
Minirest = minirest_option(Options),
minirest:generate_dispatch(Minirest).
dispatch() ->
static_dispatch() ++ dynamic_dispatch().
apps() -> apps() ->
[ [
@ -287,9 +346,6 @@ ensure_ssl_cert(Listeners = #{https := Https0 = #{ssl_options := SslOpts}}) ->
ensure_ssl_cert(Listeners) -> ensure_ssl_cert(Listeners) ->
Listeners. Listeners.
dispatch() ->
static_dispatch() ++ dynamic_dispatch().
static_dispatch() -> static_dispatch() ->
StaticFiles = ["/editor.worker.js", "/json.worker.js", "/version"], StaticFiles = ["/editor.worker.js", "/json.worker.js", "/version"],
[ [

View File

@ -25,6 +25,8 @@
-include("emqx_dashboard.hrl"). -include("emqx_dashboard.hrl").
-dialyzer({nowarn_function, [start/2]}).
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
Tables = lists:append([ Tables = lists:append([
emqx_dashboard_admin:create_tables(), emqx_dashboard_admin:create_tables(),

View File

@ -21,28 +21,14 @@
-export([execute/2]). -export([execute/2]).
execute(Req, Env) -> execute(Req, Env) ->
case check_dispatch_ready(Env) of add_cors_flag(Req, Env).
true -> add_cors_flag(Req, Env);
false -> {stop, cowboy_req:reply(503, #{<<"retry-after">> => <<"15">>}, Req)}
end.
add_cors_flag(Req, Env) -> add_cors_flag(Req, Env) ->
CORS = emqx_conf:get([dashboard, cors], false), CORS = emqx_conf:get([dashboard, cors], false),
Origin = cowboy_req:header(<<"origin">>, Req, undefined), case CORS andalso cowboy_req:header(<<"origin">>, Req, undefined) =/= undefined of
case CORS andalso Origin =/= undefined of
false -> false ->
{ok, Req, Env}; {ok, Req, Env};
true -> true ->
Req2 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>, <<"*">>, Req), Req2 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>, <<"*">>, Req),
{ok, Req2, Env} {ok, Req2, Env}
end. end.
check_dispatch_ready(Env) ->
case maps:is_key(options, Env) of
false ->
true;
true ->
%% dashboard should always ready, if not, is_ready/1 will block until ready.
%% if not ready, dashboard will return 503.
emqx_dashboard_listener:is_ready(timer:seconds(20))
end.

View File

@ -348,14 +348,7 @@ compose_filters(undefined, Filter2) ->
compose_filters(Filter1, undefined) -> compose_filters(Filter1, undefined) ->
Filter1; Filter1;
compose_filters(Filter1, Filter2) -> compose_filters(Filter1, Filter2) ->
fun(Request, RequestMeta) -> [Filter1, Filter2].
case Filter1(Request, RequestMeta) of
{ok, Request1} ->
Filter2(Request1, RequestMeta);
Response ->
Response
end
end.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Private functions %% Private functions

View File

@ -69,6 +69,9 @@ init_per_suite(Config) ->
], ],
#{work_dir => emqx_cth_suite:work_dir(Config)} #{work_dir => emqx_cth_suite:work_dir(Config)}
), ),
_ = emqx_conf_schema:roots(),
ok = emqx_dashboard_desc_cache:init(),
emqx_dashboard:save_dispatch_eterm(emqx_conf:schema_module()),
emqx_common_test_http:create_default_app(), emqx_common_test_http:create_default_app(),
[{suite_apps, SuiteApps} | Config]. [{suite_apps, SuiteApps} | Config].
@ -87,6 +90,79 @@ t_overview(_) ->
|| Overview <- ?OVERVIEWS || Overview <- ?OVERVIEWS
]. ].
t_dashboard_restart(Config) ->
emqx_config:put([dashboard], #{
i18n_lang => en,
swagger_support => true,
listeners =>
#{
http =>
#{
inet6 => false,
bind => 18083,
ipv6_v6only => false,
send_timeout => 10000,
num_acceptors => 8,
max_connections => 512,
backlog => 1024,
proxy_header => false
}
}
}),
application:stop(emqx_dashboard),
application:start(emqx_dashboard),
Name = 'http:dashboard',
t_overview(Config),
[{'_', [], Rules}] = Dispatch = persistent_term:get(Name),
%% complete dispatch has more than 150 rules.
?assertNotMatch([{[], [], cowboy_static, _} | _], Rules),
?assert(erlang:length(Rules) > 150),
CheckRules = fun(Tag) ->
[{'_', [], NewRules}] = persistent_term:get(Name, Tag),
?assertEqual(length(Rules), length(NewRules), Tag),
?assertEqual(lists:sort(Rules), lists:sort(NewRules), Tag)
end,
?check_trace(
?wait_async_action(
begin
ok = application:stop(emqx_dashboard),
?assertEqual(Dispatch, persistent_term:get(Name)),
ok = application:start(emqx_dashboard),
%% After we restart the dashboard, the dispatch rules should be the same.
CheckRules(step_1)
end,
#{?snk_kind := regenerate_minirest_dispatch},
30_000
),
fun(Trace) ->
?assertMatch([#{i18n_lang := en}], ?of_kind(regenerate_minirest_dispatch, Trace)),
%% The dispatch is updated after being regenerated.
CheckRules(step_2)
end
),
t_overview(Config),
?check_trace(
?wait_async_action(
begin
%% erase to mock the initial dashboard startup.
persistent_term:erase(Name),
ok = application:stop(emqx_dashboard),
ok = application:start(emqx_dashboard),
ct:sleep(800),
%% regenerate the dispatch rules again
CheckRules(step_3)
end,
#{?snk_kind := regenerate_minirest_dispatch},
30_000
),
fun(Trace) ->
?assertMatch([#{i18n_lang := en}], ?of_kind(regenerate_minirest_dispatch, Trace)),
CheckRules(step_4)
end
),
t_overview(Config),
ok.
t_admins_add_delete(_) -> t_admins_add_delete(_) ->
mnesia:clear_table(?ADMIN), mnesia:clear_table(?ADMIN),
Desc = <<"simple description">>, Desc = <<"simple description">>,
@ -196,28 +272,41 @@ t_disable_swagger_json(_Config) ->
{ok, {{"HTTP/1.1", 200, "OK"}, __, _}}, {ok, {{"HTTP/1.1", 200, "OK"}, __, _}},
httpc:request(get, {Url, []}, [], [{body_format, binary}]) httpc:request(get, {Url, []}, [], [{body_format, binary}])
), ),
DashboardCfg = emqx:get_raw_config([dashboard]), DashboardCfg = emqx:get_raw_config([dashboard]),
DashboardCfg2 = DashboardCfg#{<<"swagger_support">> => false},
emqx:update_config([dashboard], DashboardCfg2),
?retry(
_Sleep = 1000,
_Attempts = 5,
?assertMatch(
{ok, {{"HTTP/1.1", 404, "Not Found"}, _, _}},
httpc:request(get, {Url, []}, [], [{body_format, binary}])
)
),
DashboardCfg3 = DashboardCfg#{<<"swagger_support">> => true}, ?check_trace(
emqx:update_config([dashboard], DashboardCfg3), ?wait_async_action(
?retry( begin
_Sleep0 = 1000, DashboardCfg2 = DashboardCfg#{<<"swagger_support">> => false},
_Attempts0 = 5, emqx:update_config([dashboard], DashboardCfg2)
?assertMatch( end,
{ok, {{"HTTP/1.1", 200, "OK"}, __, _}}, #{?snk_kind := regenerate_minirest_dispatch},
httpc:request(get, {Url, []}, [], [{body_format, binary}]) 30_000
) ),
fun(Trace) ->
?assertMatch([#{i18n_lang := en}], ?of_kind(regenerate_minirest_dispatch, Trace)),
?assertMatch(
{ok, {{"HTTP/1.1", 404, "Not Found"}, _, _}},
httpc:request(get, {Url, []}, [], [{body_format, binary}])
)
end
),
?check_trace(
?wait_async_action(
begin
DashboardCfg3 = DashboardCfg#{<<"swagger_support">> => true},
emqx:update_config([dashboard], DashboardCfg3)
end,
#{?snk_kind := regenerate_minirest_dispatch},
30_000
),
fun(Trace) ->
?assertMatch([#{i18n_lang := en}], ?of_kind(regenerate_minirest_dispatch, Trace)),
?assertMatch(
{ok, {{"HTTP/1.1", 200, "OK"}, __, _}},
httpc:request(get, {Url, []}, [], [{body_format, binary}])
)
end
), ),
ok. ok.

View File

@ -20,6 +20,7 @@
-export([ -export([
api_spec/0, api_spec/0,
check_api_schema/2,
paths/0, paths/0,
schema/1, schema/1,
namespace/0 namespace/0
@ -40,11 +41,12 @@ namespace() -> "dashboard_sso".
api_spec() -> api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{ emqx_dashboard_swagger:spec(?MODULE, #{
translate_body => false, translate_body => false,
check_schema => fun(Params, Meta) -> check_schema => fun ?MODULE:check_api_schema/2
emqx_dashboard_swagger:validate_content_type(Params, Meta, <<"application/xml">>)
end
}). }).
check_api_schema(Params, Meta) ->
emqx_dashboard_swagger:validate_content_type(Params, Meta, <<"application/xml">>).
paths() -> paths() ->
[ [
"/sso/saml/acs", "/sso/saml/acs",

View File

@ -815,7 +815,8 @@ fields(mqueue_message) ->
fields(requested_client_fields) -> fields(requested_client_fields) ->
%% NOTE: some Client fields actually returned in response are missing in schema: %% NOTE: some Client fields actually returned in response are missing in schema:
%% enable_authn, is_persistent, listener, peerport %% enable_authn, is_persistent, listener, peerport
ClientFields = [element(1, F) || F <- fields(client)], ClientFields0 = [element(1, F) || F <- fields(client)],
ClientFields = [client_attrs | ClientFields0],
[ [
{fields, {fields,
hoconsc:mk( hoconsc:mk(

View File

@ -1032,6 +1032,7 @@ t_query_multiple_clients_urlencode(_) ->
t_query_clients_with_fields(_) -> t_query_clients_with_fields(_) ->
process_flag(trap_exit, true), process_flag(trap_exit, true),
TCBin = atom_to_binary(?FUNCTION_NAME), TCBin = atom_to_binary(?FUNCTION_NAME),
APIPort = 18083,
ClientId = <<TCBin/binary, "_client">>, ClientId = <<TCBin/binary, "_client">>,
Username = <<TCBin/binary, "_user">>, Username = <<TCBin/binary, "_user">>,
{ok, C} = emqtt:start_link(#{clientid => ClientId, username => Username}), {ok, C} = emqtt:start_link(#{clientid => ClientId, username => Username}),
@ -1040,6 +1041,13 @@ t_query_clients_with_fields(_) ->
Auth = emqx_mgmt_api_test_util:auth_header_(), Auth = emqx_mgmt_api_test_util:auth_header_(),
?assertEqual([#{<<"clientid">> => ClientId}], get_clients_all_fields(Auth, "fields=clientid")), ?assertEqual([#{<<"clientid">> => ClientId}], get_clients_all_fields(Auth, "fields=clientid")),
?assertMatch(
{ok,
{{_, 200, _}, _, #{
<<"data">> := [#{<<"client_attrs">> := #{}}]
}}},
list_request(APIPort, "fields=client_attrs")
),
?assertEqual( ?assertEqual(
[#{<<"clientid">> => ClientId, <<"username">> => Username}], [#{<<"clientid">> => ClientId, <<"username">> => Username}],
get_clients_all_fields(Auth, "fields=clientid,username") get_clients_all_fields(Auth, "fields=clientid,username")
@ -1072,6 +1080,7 @@ get_clients(Auth, Qs, ExpectError, ClientIdOnly) ->
Resp = emqx_mgmt_api_test_util:request_api(get, ClientsPath, Qs, Auth), Resp = emqx_mgmt_api_test_util:request_api(get, ClientsPath, Qs, Auth),
case ExpectError of case ExpectError of
false -> false ->
ct:pal("get clients response:\n ~p", [Resp]),
{ok, Body} = Resp, {ok, Body} = Resp,
#{<<"data">> := Clients} = emqx_utils_json:decode(Body), #{<<"data">> := Clients} = emqx_utils_json:decode(Body),
case ClientIdOnly of case ClientIdOnly of

View File

@ -155,6 +155,7 @@
replace/4, replace/4,
regex_match/2, regex_match/2,
regex_replace/3, regex_replace/3,
regex_extract/2,
ascii/1, ascii/1,
find/2, find/2,
find/3, find/3,
@ -805,6 +806,8 @@ regex_match(Str, RE) -> emqx_variform_bif:regex_match(Str, RE).
regex_replace(SrcStr, RE, RepStr) -> emqx_variform_bif:regex_replace(SrcStr, RE, RepStr). regex_replace(SrcStr, RE, RepStr) -> emqx_variform_bif:regex_replace(SrcStr, RE, RepStr).
regex_extract(SrcStr, RE) -> emqx_variform_bif:regex_extract(SrcStr, RE).
ascii(Char) -> emqx_variform_bif:ascii(Char). ascii(Char) -> emqx_variform_bif:ascii(Char).
find(S, P) -> emqx_variform_bif:find(S, P). find(S, P) -> emqx_variform_bif:find(S, P).

View File

@ -0,0 +1 @@
Significantly increased the startup speed of EMQX dashboard listener.

View File

@ -1,10 +1,10 @@
Respcet `clientid_prefix` config for MQTT bridges. Respcet `clientid_prefix` config for MQTT bridges.
As of version 5.4.1, EMQX limits MQTT Client ID lengths to 23 bytes. As of version 5.4.1, EMQX limits MQTT client ID lengths to 23 bytes.
Previously, the system included the `clientid_prefix` in the hash calculation of the original, excessively long Client ID, thereby impacting the resulting shortened ID. Previously, the system included the `clientid_prefix` in the hash calculation of the original unique, but long client ID, thereby impacting the resulting shortened ID.
Change Details: Change Details:
- Without Prefix: Behavior remains unchanged; EMQX will hash the entire Client ID into a 23-byte space (when longer than 23 bytes). - Without Prefix: Behavior remains unchanged; EMQX will hash the long (> 23 bytes) client ID into a 23-byte space.
- With Prefix: - With Prefix:
- Prefix no more than 19 bytes: The prefix is preserved, and the remaining suffix is hashed into a 4-byte space. - Prefix no more than 19 bytes: The prefix is preserved, and the client ID is hashed into a 4-byte space capping the length within 23 bytes.
- Prefix is 20 or more bytes: EMQX no longer attempts to shorten the Client ID, respecting the configured prefix in its entirety. - Prefix is 20 or more bytes: EMQX no longer attempts to shorten the client ID, respecting the configured prefix in its entirety.

View File

@ -0,0 +1 @@
Improved the logged error messages when an HTTP authorization request with an unsupported content-type header is returned.

View File

@ -58,7 +58,7 @@ defmodule EMQXUmbrella.MixProject do
{:ekka, github: "emqx/ekka", tag: "0.19.4", override: true}, {:ekka, github: "emqx/ekka", tag: "0.19.4", override: true},
{:gen_rpc, github: "emqx/gen_rpc", tag: "3.3.1", override: true}, {:gen_rpc, github: "emqx/gen_rpc", tag: "3.3.1", override: true},
{:grpc, github: "emqx/grpc-erl", tag: "0.6.12", override: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.12", override: true},
{:minirest, github: "emqx/minirest", tag: "1.4.1", override: true}, {:minirest, github: "emqx/minirest", tag: "1.4.3", override: true},
{:ecpool, github: "emqx/ecpool", tag: "0.5.7", override: true}, {:ecpool, github: "emqx/ecpool", tag: "0.5.7", override: true},
{:replayq, github: "emqx/replayq", tag: "0.3.8", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.8", override: true},
{:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true},

View File

@ -86,7 +86,7 @@
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.19.4"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.19.4"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "3.3.1"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "3.3.1"}}},
{grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.12"}}}, {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.12"}}},
{minirest, {git, "https://github.com/emqx/minirest", {tag, "1.4.1"}}}, {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.4.3"}}},
{ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.7"}}}, {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.7"}}},
{replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.8"}}}, {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.8"}}},
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},