Merge pull request #11137 from zhongwencool/dashboard-https-ssl-options

feat: refactor dashboard https ssl_options
This commit is contained in:
zhongwencool 2023-06-30 09:57:07 +08:00 committed by GitHub
commit 8b679cf358
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 139 additions and 31 deletions

View File

@ -2302,6 +2302,8 @@ ciphers_schema(Default) ->
#{ #{
default => default_ciphers(Default), default => default_ciphers(Default),
converter => fun converter => fun
(undefined) ->
[];
(<<>>) -> (<<>>) ->
[]; [];
("") -> ("") ->
@ -2649,6 +2651,8 @@ parse_ka_int(Bin, Name, Min, Max) ->
throw(#{reason => lists:flatten(Msg), value => I}) throw(#{reason => lists:flatten(Msg), value => I})
end. end.
user_lookup_fun_tr(undefined, Opts) ->
user_lookup_fun_tr(<<"emqx_tls_psk:lookup">>, Opts);
user_lookup_fun_tr(Lookup, #{make_serializable := true}) -> user_lookup_fun_tr(Lookup, #{make_serializable := true}) ->
fmt_user_lookup_fun(Lookup); fmt_user_lookup_fun(Lookup);
user_lookup_fun_tr(Lookup, _) -> user_lookup_fun_tr(Lookup, _) ->

View File

@ -248,9 +248,10 @@ api_key_authorize(Req, Key, Secret) ->
) )
end. end.
ensure_ssl_cert(Listeners = #{https := Https0}) -> ensure_ssl_cert(Listeners = #{https := Https0 = #{ssl_options := SslOpts}}) ->
Https1 = emqx_tls_lib:to_server_opts(tls, Https0), SslOpt1 = maps:from_list(emqx_tls_lib:to_server_opts(tls, SslOpts)),
Listeners#{https => maps:from_list(Https1)}; Https1 = maps:remove(ssl_options, Https0),
Listeners#{https => maps:merge(Https1, SslOpt1)};
ensure_ssl_cert(Listeners) -> ensure_ssl_cert(Listeners) ->
Listeners. Listeners.

View File

@ -174,17 +174,19 @@ diff_listeners(Type, Stop, Start) -> {#{Type => Stop}, #{Type => Start}}.
-define(DIR, <<"dashboard">>). -define(DIR, <<"dashboard">>).
ensure_ssl_cert(#{<<"listeners">> := #{<<"https">> := #{<<"bind">> := Bind}}} = Conf) when ensure_ssl_cert(#{<<"listeners">> := #{<<"https">> := #{<<"bind">> := Bind} = Https0}} = Conf0) when
Bind =/= 0 Bind =/= 0
-> ->
Https = emqx_utils_maps:deep_get([<<"listeners">>, <<"https">>], Conf, undefined), Https1 = emqx_dashboard_schema:https_converter(Https0, #{}),
Conf1 = emqx_utils_maps:deep_put([<<"listeners">>, <<"https">>], Conf0, Https1),
Ssl = maps:get(<<"ssl_options">>, Https1, undefined),
Opts = #{required_keys => [[<<"keyfile">>], [<<"certfile">>], [<<"cacertfile">>]]}, Opts = #{required_keys => [[<<"keyfile">>], [<<"certfile">>], [<<"cacertfile">>]]},
case emqx_tls_lib:ensure_ssl_files(?DIR, Https, Opts) of case emqx_tls_lib:ensure_ssl_files(?DIR, Ssl, Opts) of
{ok, undefined} -> {ok, undefined} ->
{error, <<"ssl_cert_not_found">>}; {error, <<"ssl_cert_not_found">>};
{ok, NewHttps} -> {ok, NewSsl} ->
{ok, Keys = [<<"listeners">>, <<"https">>, <<"ssl_options">>],
emqx_utils_maps:deep_merge(Conf, #{<<"listeners">> => #{<<"https">> => NewHttps}})}; {ok, emqx_utils_maps:deep_put(Keys, Conf1, NewSsl)};
{error, Reason} -> {error, Reason} ->
?SLOG(error, Reason#{msg => "bad_ssl_config"}), ?SLOG(error, Reason#{msg => "bad_ssl_config"}),
{error, Reason} {error, Reason}

View File

@ -21,7 +21,8 @@
roots/0, roots/0,
fields/1, fields/1,
namespace/0, namespace/0,
desc/1 desc/1,
https_converter/2
]). ]).
namespace() -> dashboard. namespace() -> dashboard.
@ -63,7 +64,8 @@ fields("dashboard") ->
desc => ?DESC(bootstrap_users_file), desc => ?DESC(bootstrap_users_file),
required => false, required => false,
default => <<>>, default => <<>>,
deprecated => {since, "5.1.0"} deprecated => {since, "5.1.0"},
importance => ?IMPORTANCE_HIDDEN
} }
)} )}
]; ];
@ -82,7 +84,8 @@ fields("listeners") ->
?R_REF("https"), ?R_REF("https"),
#{ #{
desc => "SSL listeners", desc => "SSL listeners",
required => {false, recursively} required => {false, recursively},
converter => fun ?MODULE:https_converter/2
} }
)} )}
]; ];
@ -95,11 +98,38 @@ fields("http") ->
fields("https") -> fields("https") ->
[ [
enable(false), enable(false),
bind(18084) bind(18084),
| common_listener_fields() ++ server_ssl_opts() ssl_options()
]. | common_listener_fields() ++
hidden_server_ssl_options()
];
fields("ssl_options") ->
server_ssl_options().
server_ssl_opts() -> ssl_options() ->
{"ssl_options",
?HOCON(
?R_REF("ssl_options"),
#{
required => true,
desc => ?DESC(ssl_options),
importance => ?IMPORTANCE_HIGH
}
)}.
hidden_server_ssl_options() ->
lists:map(
fun({K, V}) ->
{K, V#{
importance => ?IMPORTANCE_HIDDEN,
default => undefined,
required => false
}}
end,
server_ssl_options()
).
server_ssl_options() ->
Opts0 = emqx_schema:server_ssl_opts_schema(#{}, true), Opts0 = emqx_schema:server_ssl_opts_schema(#{}, true),
exclude_fields(["fail_if_no_peer_cert"], Opts0). exclude_fields(["fail_if_no_peer_cert"], Opts0).
@ -213,6 +243,8 @@ desc("http") ->
?DESC(desc_http); ?DESC(desc_http);
desc("https") -> desc("https") ->
?DESC(desc_https); ?DESC(desc_https);
desc("ssl_options") ->
?DESC(ssl_options);
desc(_) -> desc(_) ->
undefined. undefined.
@ -241,7 +273,7 @@ cors(desc) -> ?DESC(cors);
cors(_) -> undefined. cors(_) -> undefined.
%% TODO: change it to string type %% TODO: change it to string type
%% It will be up to the dashboard package which languagues to support %% It will be up to the dashboard package which languages to support
i18n_lang(type) -> ?ENUM([en, zh]); i18n_lang(type) -> ?ENUM([en, zh]);
i18n_lang(default) -> en; i18n_lang(default) -> en;
i18n_lang('readOnly') -> true; i18n_lang('readOnly') -> true;
@ -257,3 +289,13 @@ validate_sample_interval(Second) ->
Msg = "must be between 1 and 60 and be a divisor of 60.", Msg = "must be between 1 and 60 and be a divisor of 60.",
{error, Msg} {error, Msg}
end. end.
https_converter(Conf = #{<<"ssl_options">> := _}, _Opts) ->
Conf;
https_converter(Conf = #{}, _Opts) ->
Keys = lists:map(fun({K, _}) -> list_to_binary(K) end, server_ssl_options()),
SslOpts = maps:with(Keys, Conf),
Conf1 = maps:without(Keys, Conf),
Conf1#{<<"ssl_options">> => SslOpts};
https_converter(Conf, _Opts) ->
Conf.

View File

@ -49,7 +49,7 @@ t_update_conf(_Config) ->
Conf = #{ Conf = #{
dashboard => #{ dashboard => #{
listeners => #{ listeners => #{
https => #{bind => 18084}, https => #{bind => 18084, ssl_options => #{depth => 5}},
http => #{bind => 18083} http => #{bind => 18083}
} }
} }
@ -64,6 +64,12 @@ t_update_conf(_Config) ->
get, http_api_path(["clients"]), Headers get, http_api_path(["clients"]), Headers
), ),
Raw = emqx:get_raw_config([<<"dashboard">>]), Raw = emqx:get_raw_config([<<"dashboard">>]),
?assertEqual(
5,
emqx_utils_maps:deep_get(
[<<"listeners">>, <<"https">>, <<"ssl_options">>, <<"depth">>], Raw
)
),
?assertEqual(Client1, Client2), ?assertEqual(Client1, Client2),
?check_trace( ?check_trace(
begin begin
@ -120,7 +126,7 @@ t_default_ssl_cert(_Config) ->
validate_https(Conf, 512, default_ssl_cert(), verify_none), validate_https(Conf, 512, default_ssl_cert(), verify_none),
ok. ok.
t_normal_ssl_cert(_Config) -> t_compatibility_ssl_cert(_Config) ->
MaxConnection = 1000, MaxConnection = 1000,
Conf = #{ Conf = #{
dashboard => #{ dashboard => #{
@ -138,6 +144,29 @@ t_normal_ssl_cert(_Config) ->
validate_https(Conf, MaxConnection, default_ssl_cert(), verify_none), validate_https(Conf, MaxConnection, default_ssl_cert(), verify_none),
ok. ok.
t_normal_ssl_cert(_Config) ->
MaxConnection = 1024,
Conf = #{
dashboard => #{
listeners => #{
https => #{
bind => 18084,
ssl_options => #{
cacertfile => naive_env_interpolation(
<<"${EMQX_ETC_DIR}/certs/cacert.pem">>
),
certfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cert.pem">>),
keyfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/key.pem">>),
depth => 5
},
max_connections => MaxConnection
}
}
}
},
validate_https(Conf, MaxConnection, default_ssl_cert(), verify_none),
ok.
t_verify_cacertfile(_Config) -> t_verify_cacertfile(_Config) ->
MaxConnection = 1024, MaxConnection = 1024,
DefaultSSLCert = default_ssl_cert(), DefaultSSLCert = default_ssl_cert(),

View File

@ -222,11 +222,13 @@ t_dashboard(_Config) ->
), ),
Https2 = #{ Https2 = #{
enable => true, <<"bind">> => 18084,
bind => 18084, <<"ssl_options">> =>
keyfile => "etc/certs/badkey.pem", #{
cacertfile => "etc/certs/badcacert.pem", <<"keyfile">> => "etc/certs/badkey.pem",
certfile => "etc/certs/badcert.pem" <<"cacertfile">> => "etc/certs/badcacert.pem",
<<"certfile">> => "etc/certs/badcert.pem"
}
}, },
Dashboard2 = Dashboard#{<<"listeners">> => Listeners#{<<"https">> => Https2}}, Dashboard2 = Dashboard#{<<"listeners">> => Listeners#{<<"https">> => Https2}},
?assertMatch( ?assertMatch(
@ -240,20 +242,21 @@ t_dashboard(_Config) ->
emqx, filename:join(["etc", "certs", "cacert.pem"]) emqx, filename:join(["etc", "certs", "cacert.pem"])
), ),
Https3 = #{ Https3 = #{
<<"enable">> => true,
<<"bind">> => 18084, <<"bind">> => 18084,
<<"keyfile">> => list_to_binary(KeyFile), <<"ssl_options">> => #{
<<"cacertfile">> => list_to_binary(CacertFile), <<"keyfile">> => list_to_binary(KeyFile),
<<"certfile">> => list_to_binary(CertFile) <<"cacertfile">> => list_to_binary(CacertFile),
<<"certfile">> => list_to_binary(CertFile)
}
}, },
Dashboard3 = Dashboard#{<<"listeners">> => Listeners#{<<"https">> => Https3}}, Dashboard3 = Dashboard#{<<"listeners">> => Listeners#{<<"https">> => Https3}},
?assertMatch({ok, _}, update_config("dashboard", Dashboard3)), ?assertMatch({ok, _}, update_config("dashboard", Dashboard3)),
Dashboard4 = Dashboard#{<<"listeners">> => Listeners#{<<"https">> => #{<<"enable">> => false}}}, Dashboard4 = Dashboard#{<<"listeners">> => Listeners#{<<"https">> => #{<<"bind">> => 0}}},
?assertMatch({ok, _}, update_config("dashboard", Dashboard4)), ?assertMatch({ok, _}, update_config("dashboard", Dashboard4)),
{ok, Dashboard41} = get_config("dashboard"), {ok, Dashboard41} = get_config("dashboard"),
?assertEqual( ?assertEqual(
Https3#{<<"enable">> => false}, Https3#{<<"bind">> => 0},
read_conf([<<"dashboard">>, <<"listeners">>, <<"https">>]), read_conf([<<"dashboard">>, <<"listeners">>, <<"https">>]),
Dashboard41 Dashboard41
), ),

View File

@ -0,0 +1 @@
Refactors the dashboard listener configuration to use a nested `ssl_options` field for ssl settings.

View File

@ -7,7 +7,9 @@ backlog.label:
"""Backlog""" """Backlog"""
bind.desc: bind.desc:
"""Port without IP(18083) or port with specified IP(127.0.0.1:18083).""" """Port without IP(18083) or port with specified IP(127.0.0.1:18083).
Disabled when setting bind to `0`.
"""
bind.label: bind.label:
"""Bind""" """Bind"""
@ -136,4 +138,10 @@ token_expired_time.desc:
token_expired_time.label: token_expired_time.label:
"""Token expired time""" """Token expired time"""
ssl_options.desc:
"""SSL/TLS options for the dashboard listener."""
ssl_options.label:
"""SSL options"""
} }

View File

@ -14,6 +14,10 @@ dashboard {
listeners.http { listeners.http {
bind = 18083 bind = 18083
} }
listeners.https {
bind = 18084
depth = 5
}
} }
authentication = [ authentication = [

View File

@ -7,9 +7,23 @@ EMQX_ROOT="${EMQX_ROOT:-_build/$PROFILE/rel/emqx}"
EMQX_WAIT_FOR_START="${EMQX_WAIT_FOR_START:-30}" EMQX_WAIT_FOR_START="${EMQX_WAIT_FOR_START:-30}"
export EMQX_WAIT_FOR_START export EMQX_WAIT_FOR_START
function check_dashboard_https_ssl_options_depth() {
if [[ $1 =~ v5\.0\.25 ]]; then
EXPECT_DEPTH=5
else
EXPECT_DEPTH=10
fi
DEPTH=$("$EMQX_ROOT"/bin/emqx eval "emqx:get_config([dashboard,listeners,https,ssl_options,depth],10)")
if [[ "$DEPTH" != "$EXPECT_DEPTH" ]]; then
echo "Bad Https depth $DEPTH, expect $EXPECT_DEPTH"
exit 1
fi
}
start_emqx_with_conf() { start_emqx_with_conf() {
echo "Starting $PROFILE with $1" echo "Starting $PROFILE with $1"
"$EMQX_ROOT"/bin/emqx start "$EMQX_ROOT"/bin/emqx start
check_dashboard_https_ssl_options_depth "$1"
"$EMQX_ROOT"/bin/emqx stop "$EMQX_ROOT"/bin/emqx stop
} }