Merge remote-tracking branch 'origin/release-53' into 0928-sync-release-53
This commit is contained in:
commit
1a13b2ac56
|
@ -55,7 +55,7 @@ jobs:
|
|||
cd apps/emqx
|
||||
./rebar3 xref
|
||||
./rebar3 dialyzer
|
||||
./rebar3 eunit -v
|
||||
./rebar3 eunit -v --name 'eunit@127.0.0.1'
|
||||
./rebar3 as standalone_test ct --name 'test@127.0.0.1' -v --readable=true
|
||||
./rebar3 proper -d test/props
|
||||
- uses: actions/upload-artifact@v3
|
||||
|
|
6
Makefile
6
Makefile
|
@ -15,8 +15,8 @@ endif
|
|||
|
||||
# Dashboard version
|
||||
# from https://github.com/emqx/emqx-dashboard5
|
||||
export EMQX_DASHBOARD_VERSION ?= v1.4.1
|
||||
export EMQX_EE_DASHBOARD_VERSION ?= e1.2.1
|
||||
export EMQX_DASHBOARD_VERSION ?= v1.5.0
|
||||
export EMQX_EE_DASHBOARD_VERSION ?= e1.3.0
|
||||
|
||||
# `:=` should be used here, otherwise the `$(shell ...)` will be executed every time when the variable is used
|
||||
# In make 4.4+, for backward-compatibility the value from the original environment is used.
|
||||
|
@ -75,7 +75,7 @@ mix-deps-get: $(ELIXIR_COMMON_DEPS)
|
|||
|
||||
.PHONY: eunit
|
||||
eunit: $(REBAR) merge-config
|
||||
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c --cover_export_name $(CT_COVER_EXPORT_PREFIX)-eunit
|
||||
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit --name eunit@127.0.0.1 -v -c --cover_export_name $(CT_COVER_EXPORT_PREFIX)-eunit
|
||||
|
||||
.PHONY: proper
|
||||
proper: $(REBAR)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%% This additional config file is used when the config 'cluster.proto_dist' in emqx.conf is set to 'inet_tls'.
|
||||
%% Which means the EMQX nodes will connect to each other over TLS.
|
||||
%% For more information about inter-broker security, see: https://docs.emqx.com/en/enterprise/v5.0/deploy/cluster/security.html
|
||||
%% For more information about inter-broker security, see: https://docs.emqx.com/en/enterprise/v5.3/deploy/cluster/security.html
|
||||
|
||||
%% For more information in technical details see: http://erlang.org/doc/apps/ssl/ssl_distribution.html
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
-define(EMQX_RELEASE_CE, "5.2.1").
|
||||
|
||||
%% Enterprise edition
|
||||
-define(EMQX_RELEASE_EE, "5.3.0-alpha.1").
|
||||
-define(EMQX_RELEASE_EE, "5.3.0-alpha.2").
|
||||
|
||||
%% The HTTP API version
|
||||
-define(EMQX_API_VERSION, "5.0").
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
)
|
||||
end).
|
||||
|
||||
-define(AUDIT(_Level_, _Msg_, _Meta_), begin
|
||||
-define(AUDIT(_Level_, _From_, _Meta_), begin
|
||||
case emqx_config:get([log, audit], #{enable => false}) of
|
||||
#{enable := false} ->
|
||||
ok;
|
||||
|
@ -71,8 +71,8 @@ end).
|
|||
emqx_trace:log(
|
||||
_Level_,
|
||||
[{emqx_audit, fun(L, _) -> L end, undefined, undefined}],
|
||||
{report, _Msg_},
|
||||
_Meta_
|
||||
_Msg = undefined,
|
||||
_Meta_#{from => _From_}
|
||||
);
|
||||
gt ->
|
||||
ok
|
||||
|
|
|
@ -24,7 +24,7 @@ IsQuicSupp = fun() ->
|
|||
end,
|
||||
|
||||
Bcrypt = {bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.0"}}},
|
||||
Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.200"}}}.
|
||||
Quicer = {quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.201"}}}.
|
||||
|
||||
Dialyzer = fun(Config) ->
|
||||
{dialyzer, OldDialyzerConfig} = lists:keyfind(dialyzer, 1, Config),
|
||||
|
|
|
@ -151,13 +151,22 @@ tr_file_handlers(Conf) ->
|
|||
lists:map(fun tr_file_handler/1, Handlers).
|
||||
|
||||
tr_file_handler({HandlerName, SubConf}) ->
|
||||
FilePath = conf_get("path", SubConf),
|
||||
RotationCount = conf_get("rotation_count", SubConf),
|
||||
RotationSize = conf_get("rotation_size", SubConf),
|
||||
Type =
|
||||
case RotationSize of
|
||||
infinity -> halt;
|
||||
_ -> wrap
|
||||
end,
|
||||
HandlerConf = log_handler_conf(SubConf),
|
||||
{handler, atom(HandlerName), logger_disk_log_h, #{
|
||||
level => conf_get("level", SubConf),
|
||||
config => (log_handler_conf(SubConf))#{
|
||||
type => wrap,
|
||||
file => conf_get("path", SubConf),
|
||||
max_no_files => conf_get("rotation_count", SubConf),
|
||||
max_no_bytes => conf_get("rotation_size", SubConf)
|
||||
config => HandlerConf#{
|
||||
type => Type,
|
||||
file => FilePath,
|
||||
max_no_files => RotationCount,
|
||||
max_no_bytes => RotationSize
|
||||
},
|
||||
formatter => log_formatter(HandlerName, SubConf),
|
||||
filters => log_filter(HandlerName, SubConf),
|
||||
|
@ -216,38 +225,26 @@ log_formatter(HandlerName, Conf) ->
|
|||
end,
|
||||
SingleLine = conf_get("single_line", Conf),
|
||||
Depth = conf_get("max_depth", Conf),
|
||||
Format =
|
||||
case HandlerName of
|
||||
?AUDIT_HANDLER ->
|
||||
json;
|
||||
_ ->
|
||||
conf_get("formatter", Conf)
|
||||
end,
|
||||
do_formatter(
|
||||
HandlerName, conf_get("formatter", Conf), CharsLimit, SingleLine, TimeOffSet, Depth
|
||||
Format, CharsLimit, SingleLine, TimeOffSet, Depth
|
||||
).
|
||||
|
||||
%% helpers
|
||||
do_formatter(?AUDIT_HANDLER, _, CharsLimit, SingleLine, TimeOffSet, Depth) ->
|
||||
{emqx_logger_jsonfmt, #{
|
||||
template => [
|
||||
time,
|
||||
" [",
|
||||
level,
|
||||
"] ",
|
||||
%% http api
|
||||
{method, [code, " ", method, " ", operate_id, " ", username, " "], []},
|
||||
%% cli
|
||||
{cmd, [cmd, " "], []},
|
||||
msg,
|
||||
"\n"
|
||||
],
|
||||
chars_limit => CharsLimit,
|
||||
single_line => SingleLine,
|
||||
time_offset => TimeOffSet,
|
||||
depth => Depth
|
||||
}};
|
||||
do_formatter(_, json, CharsLimit, SingleLine, TimeOffSet, Depth) ->
|
||||
do_formatter(json, CharsLimit, SingleLine, TimeOffSet, Depth) ->
|
||||
{emqx_logger_jsonfmt, #{
|
||||
chars_limit => CharsLimit,
|
||||
single_line => SingleLine,
|
||||
time_offset => TimeOffSet,
|
||||
depth => Depth
|
||||
}};
|
||||
do_formatter(_, text, CharsLimit, SingleLine, TimeOffSet, Depth) ->
|
||||
do_formatter(text, CharsLimit, SingleLine, TimeOffSet, Depth) ->
|
||||
{emqx_logger_textfmt, #{
|
||||
template => [time, " [", level, "] ", msg, "\n"],
|
||||
chars_limit => CharsLimit,
|
||||
|
|
|
@ -678,7 +678,7 @@ return_change_result(ConfKeyPath, {{update, Req}, Opts}) ->
|
|||
case Req =/= ?TOMBSTONE_CONFIG_CHANGE_REQ of
|
||||
true ->
|
||||
#{
|
||||
config => emqx_config:get(ConfKeyPath),
|
||||
config => emqx_config:get(ConfKeyPath, undefined),
|
||||
raw_config => return_rawconf(ConfKeyPath, Opts)
|
||||
};
|
||||
false ->
|
||||
|
|
|
@ -437,6 +437,10 @@ do_start_listener(quic, ListenerName, #{bind := Bind} = Opts) ->
|
|||
case maps:get(cacertfile, SSLOpts, undefined) of
|
||||
undefined ->
|
||||
[];
|
||||
<<>> ->
|
||||
[];
|
||||
"" ->
|
||||
[];
|
||||
CaCertFile ->
|
||||
[{cacertfile, emqx_schema:naive_env_interpolation(CaCertFile)}]
|
||||
end ++
|
||||
|
|
|
@ -51,7 +51,8 @@
|
|||
-type config() :: #{
|
||||
depth => pos_integer() | unlimited,
|
||||
report_cb => logger:report_cb(),
|
||||
single_line => boolean()
|
||||
single_line => boolean(),
|
||||
chars_limit => unlimited | pos_integer()
|
||||
}.
|
||||
|
||||
-define(IS_STRING(String), (is_list(String) orelse is_binary(String))).
|
||||
|
@ -64,19 +65,17 @@
|
|||
best_effort_json(Input) ->
|
||||
best_effort_json(Input, [pretty, force_utf8]).
|
||||
best_effort_json(Input, Opts) ->
|
||||
Config = #{depth => unlimited, single_line => true},
|
||||
Config = #{depth => unlimited, single_line => true, chars_limit => unlimited},
|
||||
JsonReady = best_effort_json_obj(Input, Config),
|
||||
emqx_utils_json:encode(JsonReady, Opts).
|
||||
|
||||
-spec format(logger:log_event(), config()) -> iodata().
|
||||
format(#{level := Level, msg := Msg, meta := Meta} = Event, Config0) when is_map(Config0) ->
|
||||
format(#{level := Level, msg := Msg, meta := Meta}, Config0) when is_map(Config0) ->
|
||||
Config = add_default_config(Config0),
|
||||
MsgBin = format(Msg, Meta#{level => Level}, Config),
|
||||
logger_formatter:format(Event#{msg => {string, MsgBin}}, Config).
|
||||
[format(Msg, Meta#{level => Level}, Config), "\n"].
|
||||
|
||||
format(Msg, Meta0, Config) ->
|
||||
Meta = maps:without([time, level], Meta0),
|
||||
Data0 =
|
||||
format(Msg, Meta, Config) ->
|
||||
Data =
|
||||
try maybe_format_msg(Msg, Meta, Config) of
|
||||
Map when is_map(Map) ->
|
||||
maps:merge(Map, Meta);
|
||||
|
@ -92,9 +91,10 @@ format(Msg, Meta0, Config) ->
|
|||
fmt_stacktrace => S
|
||||
}
|
||||
end,
|
||||
Data = maps:without([report_cb], Data0),
|
||||
emqx_utils_json:encode(json_obj(Data, Config)).
|
||||
emqx_utils_json:encode(json_obj_root(Data, Config)).
|
||||
|
||||
maybe_format_msg(undefined, _Meta, _Config) ->
|
||||
#{};
|
||||
maybe_format_msg({report, Report} = Msg, #{report_cb := Cb} = Meta, Config) ->
|
||||
case is_map(Report) andalso Cb =:= ?DEFAULT_FORMATTER of
|
||||
true ->
|
||||
|
@ -128,7 +128,7 @@ format_msg({report, Report}, #{report_cb := Fun} = Meta, Config) when is_functio
|
|||
end;
|
||||
format_msg({report, Report}, #{report_cb := Fun}, Config) when is_function(Fun, 2) ->
|
||||
%% a format callback function of arity 2
|
||||
case Fun(Report, maps:with([depth, single_line], Config)) of
|
||||
case Fun(Report, maps:with([depth, single_line, chars_limit], Config)) of
|
||||
Chardata when ?IS_STRING(Chardata) ->
|
||||
try
|
||||
unicode:characters_to_binary(Chardata, utf8)
|
||||
|
@ -152,11 +152,13 @@ format_msg({Fmt, Args}, _Meta, Config) ->
|
|||
|
||||
do_format_msg(Format0, Args, #{
|
||||
depth := Depth,
|
||||
single_line := SingleLine
|
||||
single_line := SingleLine,
|
||||
chars_limit := Limit
|
||||
}) ->
|
||||
Opts = chars_limit_to_opts(Limit),
|
||||
Format1 = io_lib:scan_format(Format0, Args),
|
||||
Format = reformat(Format1, Depth, SingleLine),
|
||||
Text0 = io_lib:build_text(Format, []),
|
||||
Text0 = io_lib:build_text(Format, Opts),
|
||||
Text =
|
||||
case SingleLine of
|
||||
true -> re:replace(Text0, ",?\r?\n\s*", ", ", [{return, list}, global, unicode]);
|
||||
|
@ -164,6 +166,9 @@ do_format_msg(Format0, Args, #{
|
|||
end,
|
||||
trim(unicode:characters_to_binary(Text, utf8)).
|
||||
|
||||
chars_limit_to_opts(unlimited) -> [];
|
||||
chars_limit_to_opts(Limit) -> [{chars_limit, Limit}].
|
||||
|
||||
%% Get rid of the leading spaces.
|
||||
%% leave alone the trailing spaces.
|
||||
trim(<<$\s, Rest/binary>>) -> trim(Rest);
|
||||
|
@ -221,10 +226,6 @@ best_effort_json_obj(Map, Config) ->
|
|||
do_format_msg("~p", [Map], Config)
|
||||
end.
|
||||
|
||||
json([], _) ->
|
||||
"";
|
||||
json(<<"">>, _) ->
|
||||
"\"\"";
|
||||
json(A, _) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||
json(I, _) when is_integer(I) -> I;
|
||||
json(F, _) when is_float(F) -> F;
|
||||
|
@ -233,52 +234,76 @@ json(P, C) when is_port(P) -> json(port_to_list(P), C);
|
|||
json(F, C) when is_function(F) -> json(erlang:fun_to_list(F), C);
|
||||
json(B, Config) when is_binary(B) ->
|
||||
best_effort_unicode(B, Config);
|
||||
json(L, Config) when is_list(L), is_integer(hd(L)) ->
|
||||
best_effort_unicode(L, Config);
|
||||
json(M, Config) when is_list(M), is_tuple(hd(M)), tuple_size(hd(M)) =:= 2 ->
|
||||
best_effort_json_obj(M, Config);
|
||||
json(L, Config) when is_list(L) ->
|
||||
[json(I, Config) || I <- L];
|
||||
case lists:all(fun erlang:is_binary/1, L) of
|
||||
true ->
|
||||
%% string array
|
||||
L;
|
||||
false ->
|
||||
try unicode:characters_to_binary(L, utf8) of
|
||||
B when is_binary(B) -> B;
|
||||
_ -> [json(I, Config) || I <- L]
|
||||
catch
|
||||
_:_ ->
|
||||
[json(I, Config) || I <- L]
|
||||
end
|
||||
end;
|
||||
json(Map, Config) when is_map(Map) ->
|
||||
best_effort_json_obj(Map, Config);
|
||||
json(Term, Config) ->
|
||||
do_format_msg("~p", [Term], Config).
|
||||
|
||||
json_obj_root(Data0, Config) ->
|
||||
Time = maps:get(time, Data0, undefined),
|
||||
Level = maps:get(level, Data0, undefined),
|
||||
Msg1 =
|
||||
case maps:get(msg, Data0, undefined) of
|
||||
undefined ->
|
||||
maps:get('$kind', Data0, undefined);
|
||||
Msg0 ->
|
||||
Msg0
|
||||
end,
|
||||
Msg =
|
||||
case Msg1 of
|
||||
undefined ->
|
||||
undefined;
|
||||
_ ->
|
||||
json(Msg1, Config)
|
||||
end,
|
||||
Mfal = emqx_utils:format_mfal(Data0),
|
||||
Data =
|
||||
maps:fold(
|
||||
fun(K, V, D) ->
|
||||
{K1, V1} = json_kv(K, V, Config),
|
||||
[{K1, V1} | D]
|
||||
end,
|
||||
[],
|
||||
maps:without(
|
||||
[time, gl, file, report_cb, msg, '$kind', mfa, level, line, is_trace], Data0
|
||||
)
|
||||
),
|
||||
lists:filter(
|
||||
fun({_, V}) -> V =/= undefined end,
|
||||
[{time, Time}, {level, Level}, {msg, Msg}, {mfa, Mfal}]
|
||||
) ++ Data.
|
||||
|
||||
json_obj(Data, Config) ->
|
||||
maps:fold(
|
||||
fun(K, V, D) ->
|
||||
json_kv(K, V, D, Config)
|
||||
{K1, V1} = json_kv(K, V, Config),
|
||||
maps:put(K1, V1, D)
|
||||
end,
|
||||
maps:new(),
|
||||
Data
|
||||
).
|
||||
|
||||
json_kv(mfa, {M, F, A}, Data, _Config) ->
|
||||
maps:put(
|
||||
mfa,
|
||||
<<
|
||||
(atom_to_binary(M, utf8))/binary,
|
||||
$:,
|
||||
(atom_to_binary(F, utf8))/binary,
|
||||
$/,
|
||||
(integer_to_binary(A))/binary
|
||||
>>,
|
||||
Data
|
||||
);
|
||||
%% snabbkaffe
|
||||
json_kv('$kind', Kind, Data, Config) ->
|
||||
maps:put(msg, json(Kind, Config), Data);
|
||||
json_kv(gl, _, Data, _Config) ->
|
||||
%% drop gl because it's not interesting
|
||||
Data;
|
||||
json_kv(file, _, Data, _Config) ->
|
||||
%% drop 'file' because we have mfa
|
||||
Data;
|
||||
json_kv(K0, V, Data, Config) ->
|
||||
json_kv(K0, V, Config) ->
|
||||
K = json_key(K0),
|
||||
case is_map(V) of
|
||||
true -> maps:put(json(K, Config), best_effort_json_obj(V, Config), Data);
|
||||
false -> maps:put(json(K, Config), json(V, Config), Data)
|
||||
true -> {K, best_effort_json_obj(V, Config)};
|
||||
false -> {K, json(V, Config)}
|
||||
end.
|
||||
|
||||
json_key(A) when is_atom(A) -> json_key(atom_to_binary(A, utf8));
|
||||
|
@ -373,23 +398,83 @@ p_config() ->
|
|||
proper_types:shrink_list(
|
||||
[
|
||||
{depth, p_limit()},
|
||||
{single_line, proper_types:boolean()}
|
||||
{single_line, proper_types:boolean()},
|
||||
{chars_limit, p_limit()}
|
||||
]
|
||||
).
|
||||
|
||||
%% NOTE: pretty-printing format is asserted in the test
|
||||
%% This affects the CLI output format, consult the team before changing
|
||||
%% the format.
|
||||
best_effort_json_test() ->
|
||||
?assertEqual(
|
||||
<<"{\n \n}">>,
|
||||
emqx_logger_jsonfmt:best_effort_json([])
|
||||
best_effort_json([])
|
||||
),
|
||||
?assertEqual(
|
||||
<<"{\n \"key\" : [\n \n ]\n}">>,
|
||||
emqx_logger_jsonfmt:best_effort_json(#{key => []})
|
||||
best_effort_json(#{key => []})
|
||||
),
|
||||
?assertEqual(
|
||||
<<"[\n {\n \"key\" : [\n \n ]\n }\n]">>,
|
||||
emqx_logger_jsonfmt:best_effort_json([#{key => []}])
|
||||
best_effort_json([#{key => []}])
|
||||
),
|
||||
ok.
|
||||
|
||||
config() ->
|
||||
#{
|
||||
chars_limit => unlimited,
|
||||
depth => unlimited,
|
||||
single_line => true
|
||||
}.
|
||||
|
||||
make_log(Report) ->
|
||||
#{
|
||||
level => info,
|
||||
msg => Report,
|
||||
meta => #{time => 1111, report_cb => ?DEFAULT_FORMATTER}
|
||||
}.
|
||||
|
||||
ensure_json_output_test() ->
|
||||
JSON = format(make_log({report, #{foo => bar}}), config()),
|
||||
?assert(is_map(emqx_utils_json:decode(JSON))),
|
||||
ok.
|
||||
|
||||
chars_limit_not_applied_on_raw_map_fields_test() ->
|
||||
Limit = 32,
|
||||
Len = 100,
|
||||
LongStr = lists:duplicate(Len, $a),
|
||||
Config0 = config(),
|
||||
Config = Config0#{
|
||||
chars_limit => Limit
|
||||
},
|
||||
JSON = format(make_log({report, #{foo => LongStr}}), Config),
|
||||
#{<<"foo">> := LongStr1} = emqx_utils_json:decode(JSON),
|
||||
?assertEqual(Len, size(LongStr1)),
|
||||
ok.
|
||||
|
||||
chars_limit_applied_on_format_result_test() ->
|
||||
Limit = 32,
|
||||
Len = 100,
|
||||
LongStr = lists:duplicate(Len, $a),
|
||||
Config0 = config(),
|
||||
Config = Config0#{
|
||||
chars_limit => Limit
|
||||
},
|
||||
JSON = format(make_log({string, LongStr}), Config),
|
||||
#{<<"msg">> := LongStr1} = emqx_utils_json:decode(JSON),
|
||||
?assertEqual(Limit, size(LongStr1)),
|
||||
ok.
|
||||
|
||||
string_array_test() ->
|
||||
Array = #{<<"arr">> => [<<"a">>, <<"b">>]},
|
||||
Encoded = emqx_utils_json:encode(json(Array, config())),
|
||||
?assertEqual(Array, emqx_utils_json:decode(Encoded)).
|
||||
|
||||
iolist_test() ->
|
||||
Iolist = #{iolist => ["a", ["b"]]},
|
||||
Concat = #{<<"iolist">> => <<"ab">>},
|
||||
Encoded = emqx_utils_json:encode(json(Iolist, config())),
|
||||
?assertEqual(Concat, emqx_utils_json:decode(Encoded)).
|
||||
|
||||
-endif.
|
||||
|
|
|
@ -56,8 +56,7 @@ enrich_report(ReportRaw, Meta) ->
|
|||
end,
|
||||
ClientId = maps:get(clientid, Meta, undefined),
|
||||
Peer = maps:get(peername, Meta, undefined),
|
||||
MFA = maps:get(mfa, Meta, undefined),
|
||||
Line = maps:get(line, Meta, undefined),
|
||||
MFA = emqx_utils:format_mfal(Meta),
|
||||
Msg = maps:get(msg, ReportRaw, undefined),
|
||||
%% turn it into a list so that the order of the fields is determined
|
||||
lists:foldl(
|
||||
|
@ -70,8 +69,7 @@ enrich_report(ReportRaw, Meta) ->
|
|||
{topic, try_format_unicode(Topic)},
|
||||
{clientid, try_format_unicode(ClientId)},
|
||||
{peername, Peer},
|
||||
{line, Line},
|
||||
{mfa, mfa(MFA)},
|
||||
{mfa, try_format_unicode(MFA)},
|
||||
{msg, Msg}
|
||||
]
|
||||
).
|
||||
|
@ -84,7 +82,7 @@ try_format_unicode(Char) ->
|
|||
case unicode:characters_to_list(Char) of
|
||||
{error, _, _} -> error;
|
||||
{incomplete, _, _} -> error;
|
||||
Binary -> Binary
|
||||
List1 -> List1
|
||||
end
|
||||
catch
|
||||
_:_ ->
|
||||
|
@ -95,8 +93,8 @@ try_format_unicode(Char) ->
|
|||
_ -> List
|
||||
end.
|
||||
|
||||
enrich_mfa({Fmt, Args}, #{mfa := Mfa, line := Line}) when is_list(Fmt) ->
|
||||
{Fmt ++ " mfa: ~ts line: ~w", Args ++ [mfa(Mfa), Line]};
|
||||
enrich_mfa({Fmt, Args}, Data) when is_list(Fmt) ->
|
||||
{Fmt ++ " mfa: ~ts", Args ++ [emqx_utils:format_mfal(Data)]};
|
||||
enrich_mfa(Msg, _) ->
|
||||
Msg.
|
||||
|
||||
|
@ -113,6 +111,3 @@ enrich_topic({Fmt, Args}, #{topic := Topic}) when is_list(Fmt) ->
|
|||
{" topic: ~ts" ++ Fmt, [Topic | Args]};
|
||||
enrich_topic(Msg, _) ->
|
||||
Msg.
|
||||
|
||||
mfa(undefined) -> undefined;
|
||||
mfa({M, F, A}) -> [atom_to_list(M), ":", atom_to_list(F), "/" ++ integer_to_list(A)].
|
||||
|
|
|
@ -43,6 +43,10 @@
|
|||
erpc_multicall/1
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
-compile(
|
||||
{inline, [
|
||||
rpc_node/1,
|
||||
|
@ -75,15 +79,15 @@
|
|||
|
||||
-spec call(node(), module(), atom(), list()) -> call_result().
|
||||
call(Node, Mod, Fun, Args) ->
|
||||
filter_result(gen_rpc:call(rpc_node(Node), Mod, Fun, Args)).
|
||||
maybe_badrpc(gen_rpc:call(rpc_node(Node), Mod, Fun, Args)).
|
||||
|
||||
-spec call(term(), node(), module(), atom(), list()) -> call_result().
|
||||
call(Key, Node, Mod, Fun, Args) ->
|
||||
filter_result(gen_rpc:call(rpc_node({Key, Node}), Mod, Fun, Args)).
|
||||
maybe_badrpc(gen_rpc:call(rpc_node({Key, Node}), Mod, Fun, Args)).
|
||||
|
||||
-spec call(term(), node(), module(), atom(), list(), timeout()) -> call_result().
|
||||
call(Key, Node, Mod, Fun, Args, Timeout) ->
|
||||
filter_result(gen_rpc:call(rpc_node({Key, Node}), Mod, Fun, Args, Timeout)).
|
||||
maybe_badrpc(gen_rpc:call(rpc_node({Key, Node}), Mod, Fun, Args, Timeout)).
|
||||
|
||||
-spec multicall([node()], module(), atom(), list()) -> multicall_result().
|
||||
multicall(Nodes, Mod, Fun, Args) ->
|
||||
|
@ -127,18 +131,15 @@ rpc_nodes([], Acc) ->
|
|||
rpc_nodes([Node | Nodes], Acc) ->
|
||||
rpc_nodes(Nodes, [rpc_node(Node) | Acc]).
|
||||
|
||||
filter_result({Error, Reason}) when
|
||||
Error =:= badrpc; Error =:= badtcp
|
||||
->
|
||||
maybe_badrpc({Error, Reason}) when Error =:= badrpc; Error =:= badtcp ->
|
||||
{badrpc, Reason};
|
||||
filter_result(Delivery) ->
|
||||
maybe_badrpc(Delivery) ->
|
||||
Delivery.
|
||||
|
||||
max_client_num() ->
|
||||
emqx:get_config([rpc, tcp_client_num], ?DefaultClientNum).
|
||||
|
||||
-spec unwrap_erpc(emqx_rpc:erpc(A) | [emqx_rpc:erpc(A)]) -> A | {error, _Err} | list().
|
||||
|
||||
unwrap_erpc(Res) when is_list(Res) ->
|
||||
[unwrap_erpc(R) || R <- Res];
|
||||
unwrap_erpc({ok, A}) ->
|
||||
|
@ -151,3 +152,73 @@ unwrap_erpc({exit, Err}) ->
|
|||
{error, Err};
|
||||
unwrap_erpc({error, {erpc, Err}}) ->
|
||||
{error, Err}.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
badrpc_call_test_() ->
|
||||
application:ensure_all_started(gen_rpc),
|
||||
Node = node(),
|
||||
[
|
||||
{"throw", fun() ->
|
||||
?assertEqual(foo, call(Node, erlang, throw, [foo]))
|
||||
end},
|
||||
{"error", fun() ->
|
||||
?assertMatch({badrpc, {'EXIT', {foo, _}}}, call(Node, erlang, error, [foo]))
|
||||
end},
|
||||
{"exit", fun() ->
|
||||
?assertEqual({badrpc, {'EXIT', foo}}, call(Node, erlang, exit, [foo]))
|
||||
end},
|
||||
{"timeout", fun() ->
|
||||
?assertEqual({badrpc, timeout}, call(key, Node, timer, sleep, [1000], 100))
|
||||
end},
|
||||
{"noconnection", fun() ->
|
||||
%% mute crash report from gen_rpc
|
||||
logger:set_primary_config(level, critical),
|
||||
try
|
||||
?assertEqual(
|
||||
{badrpc, nxdomain}, call(key, 'no@such.node', foo, bar, [])
|
||||
)
|
||||
after
|
||||
logger:set_primary_config(level, notice)
|
||||
end
|
||||
end}
|
||||
].
|
||||
|
||||
multicall_test() ->
|
||||
application:ensure_all_started(gen_rpc),
|
||||
logger:set_primary_config(level, critical),
|
||||
BadNode = 'no@such.node',
|
||||
ThisNode = node(),
|
||||
Nodes = [ThisNode, BadNode],
|
||||
Call4 = fun(M, F, A) -> multicall(Nodes, M, F, A) end,
|
||||
Call5 = fun(Key, M, F, A) -> multicall(Key, Nodes, M, F, A) end,
|
||||
try
|
||||
?assertMatch({[foo], [{BadNode, _}]}, Call4(erlang, throw, [foo])),
|
||||
?assertMatch({[], [{ThisNode, _}, {BadNode, _}]}, Call4(erlang, error, [foo])),
|
||||
?assertMatch({[], [{ThisNode, _}, {BadNode, _}]}, Call4(erlang, exit, [foo])),
|
||||
?assertMatch({[], [{ThisNode, _}, {BadNode, _}]}, Call5(key, foo, bar, []))
|
||||
after
|
||||
logger:set_primary_config(level, notice)
|
||||
end.
|
||||
|
||||
unwrap_erpc_test_() ->
|
||||
Nodes = [node()],
|
||||
MultiC = fun(M, F, A) -> unwrap_erpc(erpc:multicall(Nodes, M, F, A, 100)) end,
|
||||
[
|
||||
{"throw", fun() ->
|
||||
?assertEqual([{error, foo}], MultiC(erlang, throw, [foo]))
|
||||
end},
|
||||
{"error", fun() ->
|
||||
?assertEqual([{error, foo}], MultiC(erlang, error, [foo]))
|
||||
end},
|
||||
{"exit", fun() ->
|
||||
?assertEqual([{error, {exception, foo}}], MultiC(erlang, exit, [foo]))
|
||||
end},
|
||||
{"noconnection", fun() ->
|
||||
?assertEqual(
|
||||
[{error, noconnection}], unwrap_erpc(erpc:multicall(['no@such.node'], foo, bar, []))
|
||||
)
|
||||
end}
|
||||
].
|
||||
|
||||
-endif.
|
||||
|
|
|
@ -271,9 +271,12 @@ find_config_references(Root) ->
|
|||
is_file_reference(Stack) ->
|
||||
lists:any(
|
||||
fun(KP) -> lists:prefix(lists:reverse(KP), Stack) end,
|
||||
emqx_tls_lib:ssl_file_conf_keypaths()
|
||||
conf_keypaths()
|
||||
).
|
||||
|
||||
conf_keypaths() ->
|
||||
emqx_tls_lib:ssl_file_conf_keypaths().
|
||||
|
||||
mk_fileref(AbsPath) ->
|
||||
case emqx_utils_fs:read_info(AbsPath) of
|
||||
{ok, Info} ->
|
||||
|
|
|
@ -50,11 +50,17 @@
|
|||
-define(IS_FALSE(Val), ((Val =:= false) orelse (Val =:= <<"false">>))).
|
||||
|
||||
-define(SSL_FILE_OPT_PATHS, [
|
||||
%% common ssl options
|
||||
[<<"keyfile">>],
|
||||
[<<"certfile">>],
|
||||
[<<"cacertfile">>],
|
||||
[<<"ocsp">>, <<"issuer_pem">>]
|
||||
%% OCSP
|
||||
[<<"ocsp">>, <<"issuer_pem">>],
|
||||
%% SSO
|
||||
[<<"sp_public_key">>],
|
||||
[<<"sp_private_key">>]
|
||||
]).
|
||||
|
||||
-define(SSL_FILE_OPT_PATHS_A, [
|
||||
[keyfile],
|
||||
[certfile],
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
%% (e.g. in `init_per_suite/1` / `init_per_group/2`), providing the appspecs
|
||||
%% and unique work dir for the testrun (e.g. `work_dir/1`). Save the result
|
||||
%% in a context.
|
||||
%% 3. Call `emqx_cth_sutie:stop/1` to stop the applications after the testrun
|
||||
%% 3. Call `emqx_cth_suite:stop/1` to stop the applications after the testrun
|
||||
%% finishes (e.g. in `end_per_suite/1` / `end_per_group/2`), providing the
|
||||
%% result from step 2.
|
||||
-module(emqx_cth_suite).
|
||||
|
@ -245,6 +245,9 @@ spec_fmt(ffun, {_, X}) -> X.
|
|||
|
||||
maybe_configure_app(_App, #{config := false}) ->
|
||||
ok;
|
||||
maybe_configure_app(_App, AppConfig = #{schema_mod := SchemaModule}) when is_atom(SchemaModule) ->
|
||||
#{config := Config} = AppConfig,
|
||||
configure_app(SchemaModule, Config);
|
||||
maybe_configure_app(App, #{config := Config}) ->
|
||||
case app_schema(App) of
|
||||
{ok, SchemaModule} ->
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2023 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(prop_emqx_rpc).
|
||||
|
||||
-include_lib("proper/include/proper.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(NODENAME, 'test@127.0.0.1').
|
||||
|
||||
-define(ALL(Vars, Types, Exprs),
|
||||
?SETUP(
|
||||
fun() ->
|
||||
State = do_setup(),
|
||||
fun() -> do_teardown(State) end
|
||||
end,
|
||||
?FORALL(Vars, Types, Exprs)
|
||||
)
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Properties
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
prop_node() ->
|
||||
?ALL(
|
||||
Node0,
|
||||
nodename(),
|
||||
begin
|
||||
Node = punch(Node0),
|
||||
?assert(emqx_rpc:cast(Node, erlang, system_time, [])),
|
||||
case emqx_rpc:call(Node, erlang, system_time, []) of
|
||||
{badrpc, _Reason} -> true;
|
||||
Delivery when is_integer(Delivery) -> true;
|
||||
_Other -> false
|
||||
end
|
||||
end
|
||||
).
|
||||
|
||||
prop_node_with_key() ->
|
||||
?ALL(
|
||||
{Node0, Key},
|
||||
nodename_with_key(),
|
||||
begin
|
||||
Node = punch(Node0),
|
||||
?assert(emqx_rpc:cast(Key, Node, erlang, system_time, [])),
|
||||
case emqx_rpc:call(Key, Node, erlang, system_time, []) of
|
||||
{badrpc, _Reason} -> true;
|
||||
Delivery when is_integer(Delivery) -> true;
|
||||
_Other -> false
|
||||
end
|
||||
end
|
||||
).
|
||||
|
||||
prop_nodes() ->
|
||||
?ALL(
|
||||
Nodes0,
|
||||
nodesname(),
|
||||
begin
|
||||
Nodes = punch(Nodes0),
|
||||
case emqx_rpc:multicall(Nodes, erlang, system_time, []) of
|
||||
{RealResults, RealBadNodes} when
|
||||
is_list(RealResults);
|
||||
is_list(RealBadNodes)
|
||||
->
|
||||
true;
|
||||
_Other ->
|
||||
false
|
||||
end
|
||||
end
|
||||
).
|
||||
|
||||
prop_nodes_with_key() ->
|
||||
?ALL(
|
||||
{Nodes0, Key},
|
||||
nodesname_with_key(),
|
||||
begin
|
||||
Nodes = punch(Nodes0),
|
||||
case emqx_rpc:multicall(Key, Nodes, erlang, system_time, []) of
|
||||
{RealResults, RealBadNodes} when
|
||||
is_list(RealResults);
|
||||
is_list(RealBadNodes)
|
||||
->
|
||||
true;
|
||||
_Other ->
|
||||
false
|
||||
end
|
||||
end
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helper
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
do_setup() ->
|
||||
ensure_distributed_nodename(),
|
||||
ok = logger:set_primary_config(#{level => warning}),
|
||||
{ok, _Apps} = application:ensure_all_started(gen_rpc),
|
||||
ok = application:set_env(gen_rpc, call_receive_timeout, 100),
|
||||
ok = meck:new(gen_rpc, [passthrough, no_history]),
|
||||
ok = meck:expect(
|
||||
gen_rpc,
|
||||
multicall,
|
||||
fun(Nodes, Mod, Fun, Args) ->
|
||||
gen_rpc:multicall(Nodes, Mod, Fun, Args, 100)
|
||||
end
|
||||
).
|
||||
|
||||
do_teardown(_) ->
|
||||
ok = net_kernel:stop(),
|
||||
ok = application:stop(gen_rpc),
|
||||
ok = meck:unload(gen_rpc),
|
||||
%% wait for tcp close
|
||||
timer:sleep(2500).
|
||||
|
||||
ensure_distributed_nodename() ->
|
||||
case net_kernel:start([?NODENAME]) of
|
||||
{ok, _} ->
|
||||
ok;
|
||||
{error, {already_started, _}} ->
|
||||
net_kernel:stop(),
|
||||
net_kernel:start([?NODENAME]);
|
||||
{error, {{shutdown, {_, _, {'EXIT', nodistribution}}}, _}} ->
|
||||
%% start epmd first
|
||||
spawn_link(fun() -> os:cmd("epmd") end),
|
||||
timer:sleep(100),
|
||||
net_kernel:start([?NODENAME])
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Generator
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
nodename() ->
|
||||
?LET(
|
||||
{NodePrefix, HostName},
|
||||
{node_prefix(), hostname()},
|
||||
begin
|
||||
Node = NodePrefix ++ "@" ++ HostName,
|
||||
list_to_atom(Node)
|
||||
end
|
||||
).
|
||||
|
||||
nodename_with_key() ->
|
||||
?LET(
|
||||
{NodePrefix, HostName, Key},
|
||||
{node_prefix(), hostname(), choose(0, 10)},
|
||||
begin
|
||||
Node = NodePrefix ++ "@" ++ HostName,
|
||||
{list_to_atom(Node), Key}
|
||||
end
|
||||
).
|
||||
|
||||
nodesname() ->
|
||||
oneof([list(nodename()), [node()]]).
|
||||
|
||||
nodesname_with_key() ->
|
||||
oneof([{list(nodename()), choose(0, 10)}, {[node()], 1}]).
|
||||
|
||||
node_prefix() ->
|
||||
oneof(["emqxct", text_like()]).
|
||||
|
||||
text_like() ->
|
||||
?SUCHTHAT(Text, list(range($a, $z)), (length(Text) =< 100 andalso length(Text) > 0)).
|
||||
|
||||
hostname() ->
|
||||
oneof(["127.0.0.1", "localhost"]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Utils
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% After running the props, the `node()` () is only able to return an
|
||||
%% incorrect node name - `nonode@nohost`, But we want a distributed nodename
|
||||
%% So, just translate the `nonode@nohost` to ?NODENAME
|
||||
punch(Nodes) when is_list(Nodes) ->
|
||||
lists:map(fun punch/1, Nodes);
|
||||
punch('nonode@nohost') ->
|
||||
%% Equal to ?NODENAME
|
||||
node();
|
||||
punch(GoodBoy) ->
|
||||
GoodBoy.
|
|
@ -139,6 +139,7 @@ kafka_consumer_test() ->
|
|||
ok.
|
||||
|
||||
message_key_dispatch_validations_test() ->
|
||||
Name = myproducer,
|
||||
Conf0 = kafka_producer_new_hocon(),
|
||||
Conf1 =
|
||||
Conf0 ++
|
||||
|
@ -155,7 +156,7 @@ message_key_dispatch_validations_test() ->
|
|||
<<"message">> := #{<<"key">> := <<>>}
|
||||
}
|
||||
},
|
||||
emqx_utils_maps:deep_get([<<"bridges">>, <<"kafka">>, <<"myproducer">>], Conf)
|
||||
emqx_utils_maps:deep_get([<<"bridges">>, <<"kafka">>, atom_to_binary(Name)], Conf)
|
||||
),
|
||||
?assertThrow(
|
||||
{_, [
|
||||
|
@ -166,8 +167,6 @@ message_key_dispatch_validations_test() ->
|
|||
]},
|
||||
check(Conf)
|
||||
),
|
||||
%% ensure atoms exist
|
||||
_ = [myproducer],
|
||||
?assertThrow(
|
||||
{_, [
|
||||
#{
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
%%===========================================================================
|
||||
|
||||
pulsar_producer_validations_test() ->
|
||||
Name = my_producer,
|
||||
Conf0 = pulsar_producer_hocon(),
|
||||
Conf1 =
|
||||
Conf0 ++
|
||||
|
@ -24,7 +25,7 @@ pulsar_producer_validations_test() ->
|
|||
<<"strategy">> := <<"key_dispatch">>,
|
||||
<<"message">> := #{<<"key">> := <<>>}
|
||||
},
|
||||
emqx_utils_maps:deep_get([<<"bridges">>, <<"pulsar_producer">>, <<"my_producer">>], Conf)
|
||||
emqx_utils_maps:deep_get([<<"bridges">>, <<"pulsar_producer">>, atom_to_binary(Name)], Conf)
|
||||
),
|
||||
?assertThrow(
|
||||
{_, [
|
||||
|
@ -35,8 +36,6 @@ pulsar_producer_validations_test() ->
|
|||
]},
|
||||
check(Conf)
|
||||
),
|
||||
%% ensure atoms exist
|
||||
_ = [my_producer],
|
||||
?assertThrow(
|
||||
{_, [
|
||||
#{
|
||||
|
|
|
@ -108,7 +108,17 @@ admins(_) ->
|
|||
emqx_ctl:usage(usage_sync()).
|
||||
|
||||
audit(Level, From, Log) ->
|
||||
?AUDIT(Level, From, Log#{time => logger:timestamp()}).
|
||||
Log1 = redact(Log#{time => logger:timestamp()}),
|
||||
?AUDIT(Level, From, Log1).
|
||||
|
||||
redact(Logs = #{cmd := admins, args := ["add", Username, _Password | Rest]}) ->
|
||||
Logs#{args => ["add", Username, "******" | Rest]};
|
||||
redact(Logs = #{cmd := admins, args := ["passwd", Username, _Password]}) ->
|
||||
Logs#{args => ["passwd", Username, "******"]};
|
||||
redact(Logs = #{cmd := license, args := ["update", _License]}) ->
|
||||
Logs#{args => ["update", "******"]};
|
||||
redact(Logs) ->
|
||||
Logs.
|
||||
|
||||
usage_conf() ->
|
||||
[
|
||||
|
|
|
@ -43,6 +43,9 @@
|
|||
]).
|
||||
-export([conf_get/2, conf_get/3, keys/2, filter/1]).
|
||||
|
||||
%% internal exports for `emqx_enterprise_schema' only.
|
||||
-export([ensure_unicode_path/2, convert_rotation/2, log_handler_common_confs/2]).
|
||||
|
||||
%% Static apps which merge their configs into the merged emqx.conf
|
||||
%% The list can not be made a dynamic read at run-time as it is used
|
||||
%% by nodetool to generate app.<time>.config before EMQX is started
|
||||
|
@ -964,15 +967,6 @@ fields("log") ->
|
|||
aliases => [file_handlers],
|
||||
importance => ?IMPORTANCE_HIGH
|
||||
}
|
||||
)},
|
||||
{"audit",
|
||||
sc(
|
||||
?R_REF("log_audit_handler"),
|
||||
#{
|
||||
desc => ?DESC("log_audit_handler"),
|
||||
importance => ?IMPORTANCE_HIGH,
|
||||
default => #{<<"enable">> => true, <<"level">> => <<"info">>}
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields("console_handler") ->
|
||||
|
@ -1014,49 +1008,6 @@ fields("log_file_handler") ->
|
|||
}
|
||||
)}
|
||||
] ++ log_handler_common_confs(file, #{});
|
||||
fields("log_audit_handler") ->
|
||||
[
|
||||
{"path",
|
||||
sc(
|
||||
file(),
|
||||
#{
|
||||
desc => ?DESC("audit_file_handler_path"),
|
||||
default => <<"${EMQX_LOG_DIR}/audit.log">>,
|
||||
importance => ?IMPORTANCE_HIGH,
|
||||
converter => fun(Path, Opts) ->
|
||||
emqx_schema:naive_env_interpolation(ensure_unicode_path(Path, Opts))
|
||||
end
|
||||
}
|
||||
)},
|
||||
{"rotation_count",
|
||||
sc(
|
||||
range(1, 128),
|
||||
#{
|
||||
default => 10,
|
||||
converter => fun convert_rotation/2,
|
||||
desc => ?DESC("log_rotation_count"),
|
||||
importance => ?IMPORTANCE_MEDIUM
|
||||
}
|
||||
)},
|
||||
{"rotation_size",
|
||||
sc(
|
||||
hoconsc:union([infinity, emqx_schema:bytesize()]),
|
||||
#{
|
||||
default => <<"50MB">>,
|
||||
desc => ?DESC("log_file_handler_max_size"),
|
||||
importance => ?IMPORTANCE_MEDIUM
|
||||
}
|
||||
)}
|
||||
] ++
|
||||
%% Only support json
|
||||
lists:keydelete(
|
||||
"formatter",
|
||||
1,
|
||||
log_handler_common_confs(
|
||||
file,
|
||||
#{level => info, level_desc => "audit_handler_level"}
|
||||
)
|
||||
);
|
||||
fields("log_overload_kill") ->
|
||||
[
|
||||
{"enable",
|
||||
|
@ -1147,8 +1098,6 @@ desc("console_handler") ->
|
|||
?DESC("desc_console_handler");
|
||||
desc("log_file_handler") ->
|
||||
?DESC("desc_log_file_handler");
|
||||
desc("log_audit_handler") ->
|
||||
?DESC("desc_audit_log_handler");
|
||||
desc("log_rotation") ->
|
||||
?DESC("desc_log_rotation");
|
||||
desc("log_overload_kill") ->
|
||||
|
@ -1314,6 +1263,7 @@ log_handler_common_confs(Handler, Default) ->
|
|||
sc(
|
||||
hoconsc:enum([text, json]),
|
||||
#{
|
||||
aliases => [format],
|
||||
default => maps:get(formatter, Default, text),
|
||||
desc => ?DESC("common_handler_formatter"),
|
||||
importance => ?IMPORTANCE_MEDIUM
|
||||
|
|
|
@ -19,45 +19,51 @@
|
|||
-compile(export_all).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
%% erlfmt-ignore
|
||||
-define(BASE_CONF,
|
||||
"""
|
||||
node {
|
||||
name = \"emqx1@127.0.0.1\"
|
||||
cookie = \"emqxsecretcookie\"
|
||||
data_dir = \"data\"
|
||||
}
|
||||
cluster {
|
||||
name = emqxcl
|
||||
discovery_strategy = static
|
||||
static.seeds = \"emqx1@127.0.0.1\"
|
||||
core_nodes = \"emqx1@127.0.0.1\"
|
||||
}
|
||||
log {
|
||||
console {
|
||||
enable = true
|
||||
level = debug
|
||||
}
|
||||
file {
|
||||
enable = true
|
||||
level = info
|
||||
path = \"log/emqx.log\"
|
||||
}
|
||||
}
|
||||
log {
|
||||
console {
|
||||
enable = true
|
||||
level = debug
|
||||
}
|
||||
file {
|
||||
enable = true
|
||||
level = info
|
||||
path = \"log/emqx.log\"
|
||||
}
|
||||
}
|
||||
""").
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_common_test_helpers:load_config(emqx_conf_schema, iolist_to_binary(?BASE_CONF)),
|
||||
emqx_mgmt_api_test_util:init_suite([emqx_conf]),
|
||||
Config.
|
||||
Apps = emqx_cth_suite:start(
|
||||
[
|
||||
emqx,
|
||||
{emqx_conf, ?BASE_CONF}
|
||||
],
|
||||
#{work_dir => emqx_cth_suite:work_dir(Config)}
|
||||
),
|
||||
[{apps, Apps} | Config].
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_mgmt_api_test_util:end_suite([emqx_conf]).
|
||||
end_per_suite(Config) ->
|
||||
Apps = ?config(apps, Config),
|
||||
ok = emqx_cth_suite:stop(Apps),
|
||||
ok.
|
||||
|
||||
init_per_testcase(_TestCase, Config) ->
|
||||
LogConfRaw = emqx_conf:get_raw([<<"log">>]),
|
||||
[{log_conf_raw, LogConfRaw} | Config].
|
||||
|
||||
end_per_testcase(_TestCase, Config) ->
|
||||
LogConfRaw = ?config(log_conf_raw, Config),
|
||||
{ok, _} = emqx_conf:update([<<"log">>], LogConfRaw, #{}),
|
||||
ok.
|
||||
|
||||
t_log_conf(_Conf) ->
|
||||
FileExpect = #{
|
||||
|
@ -78,16 +84,7 @@ t_log_conf(_Conf) ->
|
|||
<<"time_offset">> => <<"system">>
|
||||
},
|
||||
<<"file">> =>
|
||||
#{<<"default">> => FileExpect},
|
||||
<<"audit">> =>
|
||||
#{
|
||||
<<"enable">> => true,
|
||||
<<"level">> => <<"info">>,
|
||||
<<"path">> => <<"log/audit.log">>,
|
||||
<<"rotation_count">> => 10,
|
||||
<<"rotation_size">> => <<"50MB">>,
|
||||
<<"time_offset">> => <<"system">>
|
||||
}
|
||||
#{<<"default">> => FileExpect}
|
||||
},
|
||||
?assertEqual(ExpectLog1, emqx_conf:get_raw([<<"log">>])),
|
||||
UpdateLog0 = emqx_utils_maps:deep_remove([<<"file">>, <<"default">>], ExpectLog1),
|
||||
|
@ -118,3 +115,23 @@ t_log_conf(_Conf) ->
|
|||
?assertMatch({error, {not_found, default}}, logger:get_handler_config(default)),
|
||||
?assertMatch({error, {not_found, console}}, logger:get_handler_config(console)),
|
||||
ok.
|
||||
|
||||
t_file_logger_infinity_rotation(_Config) ->
|
||||
ConfPath = [<<"log">>],
|
||||
FileConfPath = [<<"file">>, <<"default">>],
|
||||
ConfRaw = emqx_conf:get_raw(ConfPath),
|
||||
FileConfRaw = emqx_utils_maps:deep_get(FileConfPath, ConfRaw),
|
||||
%% inconsistent config: infinity rotation size, but finite rotation count
|
||||
BadFileConfRaw = maps:merge(
|
||||
FileConfRaw,
|
||||
#{
|
||||
<<"rotation_size">> => <<"infinity">>,
|
||||
<<"rotation_count">> => 10
|
||||
}
|
||||
),
|
||||
BadConfRaw = emqx_utils_maps:deep_put(FileConfPath, ConfRaw, BadFileConfRaw),
|
||||
?assertMatch({ok, _}, emqx_conf:update(ConfPath, BadConfRaw, #{})),
|
||||
HandlerIds = logger:get_handler_ids(),
|
||||
%% ensure that the handler is correctly added
|
||||
?assert(lists:member(default, HandlerIds), #{handler_ids => HandlerIds}),
|
||||
ok.
|
||||
|
|
|
@ -181,23 +181,8 @@ validate_log(Conf) ->
|
|||
}},
|
||||
FileHandler
|
||||
),
|
||||
AuditHandler = lists:keyfind(emqx_audit, 2, FileHandlers),
|
||||
%% default is enable and log level is info.
|
||||
?assertMatch(
|
||||
{handler, emqx_audit, logger_disk_log_h, #{
|
||||
config := #{
|
||||
type := wrap,
|
||||
file := "log/audit.log",
|
||||
max_no_bytes := _,
|
||||
max_no_files := _
|
||||
},
|
||||
filesync_repeat_interval := no_repeat,
|
||||
filters := [{filter_audit, {_, stop}}],
|
||||
formatter := _,
|
||||
level := info
|
||||
}},
|
||||
AuditHandler
|
||||
),
|
||||
%% audit is an EE-only feature
|
||||
?assertNot(lists:keyfind(emqx_audit, 2, FileHandlers)),
|
||||
ConsoleHandler = lists:keyfind(logger_std_h, 3, Loggers),
|
||||
?assertEqual(
|
||||
{handler, console, logger_std_h, #{
|
||||
|
@ -209,6 +194,59 @@ validate_log(Conf) ->
|
|||
ConsoleHandler
|
||||
).
|
||||
|
||||
%% erlfmt-ignore
|
||||
-define(FILE_LOG_BASE_CONF,
|
||||
"""
|
||||
log.file.default {
|
||||
enable = true
|
||||
file = \"log/xx-emqx.log\"
|
||||
formatter = text
|
||||
level = debug
|
||||
rotation_count = ~s
|
||||
rotation_size = ~s
|
||||
time_offset = \"+01:00\"
|
||||
}
|
||||
"""
|
||||
).
|
||||
|
||||
file_log_infinity_rotation_size_test_() ->
|
||||
ensure_acl_conf(),
|
||||
BaseConf = to_bin(?BASE_CONF, ["emqx1@127.0.0.1", "emqx1@127.0.0.1"]),
|
||||
Gen = fun(#{count := Count, size := Size}) ->
|
||||
Conf0 = to_bin(?FILE_LOG_BASE_CONF, [Count, Size]),
|
||||
Conf1 = [BaseConf, Conf0],
|
||||
{ok, Conf} = hocon:binary(Conf1, #{format => richmap}),
|
||||
ConfList = hocon_tconf:generate(emqx_conf_schema, Conf),
|
||||
Kernel = proplists:get_value(kernel, ConfList),
|
||||
Loggers = proplists:get_value(logger, Kernel),
|
||||
FileHandlers = lists:filter(fun(L) -> element(3, L) =:= logger_disk_log_h end, Loggers),
|
||||
lists:keyfind(default, 2, FileHandlers)
|
||||
end,
|
||||
[
|
||||
{"base conf: finite log (type = wrap)",
|
||||
?_assertMatch(
|
||||
{handler, default, logger_disk_log_h, #{
|
||||
config := #{
|
||||
type := wrap,
|
||||
max_no_bytes := 1073741824,
|
||||
max_no_files := 20
|
||||
}
|
||||
}},
|
||||
Gen(#{count => "20", size => "\"1024MB\""})
|
||||
)},
|
||||
{"rotation size = infinity (type = halt)",
|
||||
?_assertMatch(
|
||||
{handler, default, logger_disk_log_h, #{
|
||||
config := #{
|
||||
type := halt,
|
||||
max_no_bytes := infinity,
|
||||
max_no_files := 9
|
||||
}
|
||||
}},
|
||||
Gen(#{count => "9", size => "\"infinity\""})
|
||||
)}
|
||||
].
|
||||
|
||||
%% erlfmt-ignore
|
||||
-define(KERNEL_LOG_CONF,
|
||||
"""
|
||||
|
|
|
@ -145,8 +145,8 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
|
|||
|
||||
audit_log(
|
||||
audit_level(Result, Duration),
|
||||
"from_cli",
|
||||
#{duration_ms => Duration, result => Result, cmd => Cmd, args => Args, node => node()}
|
||||
cli,
|
||||
#{duration_ms => Duration, cmd => Cmd, args => Args, node => node()}
|
||||
),
|
||||
Result.
|
||||
|
||||
|
@ -350,8 +350,6 @@ audit_log(Level, From, Log) ->
|
|||
|
||||
-define(TOO_SLOW, 3000).
|
||||
|
||||
audit_level(ok, Duration) when Duration >= ?TOO_SLOW -> warning;
|
||||
audit_level({ok, _}, Duration) when Duration >= ?TOO_SLOW -> warning;
|
||||
audit_level(ok, _Duration) -> info;
|
||||
audit_level({ok, _}, _Duration) -> info;
|
||||
audit_level(_, _) -> error.
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
-define(ROLE_SUPERUSER, <<"administrator">>).
|
||||
-define(ROLE_DEFAULT, ?ROLE_SUPERUSER).
|
||||
|
||||
-define(BACKEND_LOCAL, local).
|
||||
-define(SSO_USERNAME(Backend, Name), {Backend, Name}).
|
||||
|
||||
-type dashboard_sso_backend() :: atom().
|
||||
|
|
|
@ -212,8 +212,8 @@ add_user_(Username, Password, Role, Desc) ->
|
|||
mnesia:abort(<<"username_already_exist">>)
|
||||
end.
|
||||
|
||||
-spec remove_user(binary()) -> {ok, any()} | {error, any()}.
|
||||
remove_user(Username) when is_binary(Username) ->
|
||||
-spec remove_user(dashboard_username()) -> {ok, any()} | {error, any()}.
|
||||
remove_user(Username) ->
|
||||
Trans = fun() ->
|
||||
case lookup_user(Username) of
|
||||
[] -> mnesia:abort(<<"username_not_found">>);
|
||||
|
@ -230,7 +230,7 @@ remove_user(Username) when is_binary(Username) ->
|
|||
|
||||
-spec update_user(dashboard_username(), dashboard_user_role(), binary()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
update_user(Username, Role, Desc) when is_binary(Username) ->
|
||||
update_user(Username, Role, Desc) ->
|
||||
case legal_role(Role) of
|
||||
ok ->
|
||||
case
|
||||
|
@ -427,7 +427,7 @@ flatten_username(#{username := ?SSO_USERNAME(Backend, Name)} = Data) ->
|
|||
backend => Backend
|
||||
};
|
||||
flatten_username(#{username := Username} = Data) when is_binary(Username) ->
|
||||
Data#{backend => local}.
|
||||
Data#{backend => ?BACKEND_LOCAL}.
|
||||
|
||||
-spec add_sso_user(dashboard_sso_backend(), binary(), dashboard_user_role(), binary()) ->
|
||||
{ok, map()} | {error, any()}.
|
||||
|
|
|
@ -379,9 +379,9 @@ sso_parameters() ->
|
|||
sso_parameters(Params) ->
|
||||
emqx_dashboard_sso_api:sso_parameters(Params).
|
||||
|
||||
username(#{bindings := #{backend := local}}, Username) ->
|
||||
username(#{query_string := #{<<"backend">> := ?BACKEND_LOCAL}}, Username) ->
|
||||
Username;
|
||||
username(#{bindings := #{backend := Backend}}, Username) ->
|
||||
username(#{query_string := #{<<"backend">> := Backend}}, Username) ->
|
||||
?SSO_USERNAME(Backend, Username);
|
||||
username(_Req, Username) ->
|
||||
Username.
|
||||
|
|
|
@ -25,26 +25,21 @@ log(Meta0) ->
|
|||
Duration = erlang:convert_time_unit(ReqEnd - ReqStart, native, millisecond),
|
||||
Level = level(Method, Code, Duration),
|
||||
Username = maps:get(username, Meta0, <<"">>),
|
||||
From = from(maps:get(auth_type, Meta0, "")),
|
||||
Meta1 = maps:without([req_start, req_end], Meta0),
|
||||
Meta2 = Meta1#{time => logger:timestamp(), duration_ms => Duration},
|
||||
Meta = emqx_utils:redact(Meta2),
|
||||
?AUDIT(
|
||||
Level,
|
||||
"from_api",
|
||||
Meta#{
|
||||
from => from(maps:get(auth_type, Meta0, "")),
|
||||
username => binary_to_list(Username),
|
||||
node => node()
|
||||
}
|
||||
From,
|
||||
Meta#{username => binary_to_list(Username), node => node()}
|
||||
),
|
||||
ok.
|
||||
|
||||
from(jwt_token) -> "dashboard";
|
||||
from(api_key) -> "aip_key";
|
||||
from(_) -> "unauthorized".
|
||||
from(_) -> "rest_api".
|
||||
|
||||
level(_, _Code, Duration) when Duration > 3000 -> warning;
|
||||
level(get, Code, _) when Code >= 200 andalso Code < 300 -> debug;
|
||||
level(get, _Code, _) -> debug;
|
||||
level(_, Code, _) when Code >= 200 andalso Code < 300 -> info;
|
||||
level(_, Code, _) when Code >= 300 andalso Code < 400 -> warning;
|
||||
level(_, Code, _) when Code >= 400 andalso Code < 500 -> error;
|
||||
|
|
|
@ -24,9 +24,26 @@
|
|||
unload/0
|
||||
]).
|
||||
|
||||
load() ->
|
||||
emqx_ctl:register_command(admins, {?MODULE, admins}, []).
|
||||
-export([bin/1, print_error/1]).
|
||||
|
||||
-if(?EMQX_RELEASE_EDITION == ee).
|
||||
-define(CLI_MOD, emqx_dashboard_sso_cli).
|
||||
-else.
|
||||
-define(CLI_MOD, ?MODULE).
|
||||
-endif.
|
||||
|
||||
load() ->
|
||||
emqx_ctl:register_command(admins, {?CLI_MOD, admins}, []).
|
||||
|
||||
admins(["add", Username, Password]) ->
|
||||
admins(["add", Username, Password, ""]);
|
||||
admins(["add", Username, Password, Desc]) ->
|
||||
case emqx_dashboard_admin:add_user(bin(Username), bin(Password), ?ROLE_DEFAULT, bin(Desc)) of
|
||||
{ok, _} ->
|
||||
emqx_ctl:print("ok~n");
|
||||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end;
|
||||
admins(["passwd", Username, Password]) ->
|
||||
case emqx_dashboard_admin:change_password(bin(Username), bin(Password)) of
|
||||
{ok, _} ->
|
||||
|
@ -41,8 +58,14 @@ admins(["del", Username]) ->
|
|||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end;
|
||||
admins(Args) ->
|
||||
inner_admins(Args).
|
||||
admins(_) ->
|
||||
emqx_ctl:usage(
|
||||
[
|
||||
{"admins add <Username> <Password> <Description>", "Add dashboard user"},
|
||||
{"admins passwd <Username> <Password>", "Reset dashboard user password"},
|
||||
{"admins del <Username>", "Delete dashboard user"}
|
||||
]
|
||||
).
|
||||
|
||||
unload() ->
|
||||
emqx_ctl:unregister_command(admins).
|
||||
|
@ -54,47 +77,3 @@ print_error(Reason) when is_binary(Reason) ->
|
|||
%% Maybe has more types of error, but there is only binary now. So close it for dialyzer.
|
||||
% print_error(Reason) ->
|
||||
% emqx_ctl:print("Error: ~p~n", [Reason]).
|
||||
|
||||
-if(?EMQX_RELEASE_EDITION == ee).
|
||||
usage() ->
|
||||
[
|
||||
{"admins add <Username> <Password> <Role> <Description>", "Add dashboard user"},
|
||||
{"admins passwd <Username> <Password>", "Reset dashboard user password"},
|
||||
{"admins del <Username>", "Delete dashboard user"}
|
||||
].
|
||||
|
||||
inner_admins(["add", Username, Password]) ->
|
||||
inner_admins(["add", Username, Password, ?ROLE_SUPERUSER]);
|
||||
inner_admins(["add", Username, Password, Role]) ->
|
||||
inner_admins(["add", Username, Password, Role, ""]);
|
||||
inner_admins(["add", Username, Password, Role, Desc]) ->
|
||||
case emqx_dashboard_admin:add_user(bin(Username), bin(Password), bin(Role), bin(Desc)) of
|
||||
{ok, _} ->
|
||||
emqx_ctl:print("ok~n");
|
||||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end;
|
||||
inner_admins(_) ->
|
||||
emqx_ctl:usage(usage()).
|
||||
-else.
|
||||
|
||||
usage() ->
|
||||
[
|
||||
{"admins add <Username> <Password> <Description>", "Add dashboard user"},
|
||||
{"admins passwd <Username> <Password>", "Reset dashboard user password"},
|
||||
{"admins del <Username>", "Delete dashboard user"}
|
||||
].
|
||||
|
||||
inner_admins(["add", Username, Password]) ->
|
||||
inner_admins(["add", Username, Password, ""]);
|
||||
inner_admins(["add", Username, Password, Desc]) ->
|
||||
case emqx_dashboard_admin:add_user(bin(Username), bin(Password), ?ROLE_SUPERUSER, bin(Desc)) of
|
||||
{ok, _} ->
|
||||
emqx_ctl:print("ok~n");
|
||||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end;
|
||||
inner_admins(_) ->
|
||||
emqx_ctl:usage(usage()).
|
||||
|
||||
-endif.
|
||||
|
|
|
@ -68,7 +68,7 @@ fields("dashboard") ->
|
|||
importance => ?IMPORTANCE_HIDDEN
|
||||
}
|
||||
)}
|
||||
];
|
||||
] ++ sso_fields();
|
||||
fields("listeners") ->
|
||||
[
|
||||
{"http",
|
||||
|
@ -299,3 +299,18 @@ https_converter(Conf = #{}, _Opts) ->
|
|||
Conf1#{<<"ssl_options">> => SslOpts};
|
||||
https_converter(Conf, _Opts) ->
|
||||
Conf.
|
||||
|
||||
-if(?EMQX_RELEASE_EDITION == ee).
|
||||
sso_fields() ->
|
||||
[
|
||||
{sso,
|
||||
?HOCON(
|
||||
?R_REF(emqx_dashboard_sso_schema, sso),
|
||||
#{required => {false, recursively}}
|
||||
)}
|
||||
].
|
||||
|
||||
-else.
|
||||
sso_fields() ->
|
||||
[].
|
||||
-endif.
|
||||
|
|
|
@ -191,7 +191,7 @@ token_ttl() ->
|
|||
format(Token, ?SSO_USERNAME(Backend, Name), Role, ExpTime) ->
|
||||
format(Token, Backend, Name, Role, ExpTime);
|
||||
format(Token, Username, Role, ExpTime) ->
|
||||
format(Token, local, Username, Role, ExpTime).
|
||||
format(Token, ?BACKEND_LOCAL, Username, Role, ExpTime).
|
||||
|
||||
format(Token, Backend, Username, Role, ExpTime) ->
|
||||
#?ADMIN_JWT{
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
{deps, [
|
||||
{emqx_ldap, {path, "../../apps/emqx_ldap"}},
|
||||
{emqx_dashboard, {path, "../../apps/emqx_dashboard"}},
|
||||
{esaml, {git, "https://github.com/emqx/esaml", {tag, "v1.1.1"}}}
|
||||
{esaml, {git, "https://github.com/emqx/esaml", {tag, "v1.1.2"}}}
|
||||
]}.
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
create/2,
|
||||
update/3,
|
||||
destroy/2,
|
||||
login/3
|
||||
login/3,
|
||||
convert_certs/3
|
||||
]).
|
||||
|
||||
-export([types/0, modules/0, provider/1, backends/0]).
|
||||
-export([types/0, modules/0, provider/1, backends/0, format/1]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Callbacks
|
||||
|
@ -26,7 +27,9 @@
|
|||
backend => atom(),
|
||||
atom() => term()
|
||||
}.
|
||||
-type state() :: #{atom() => term()}.
|
||||
|
||||
%% Note: if a backend has a resource, it must be stored in the state and named resource_id
|
||||
-type state() :: #{resource_id => binary(), atom() => term()}.
|
||||
-type raw_config() :: #{binary() => term()}.
|
||||
-type config() :: parsed_config() | raw_config().
|
||||
-type hocon_ref() :: ?R_REF(Module :: atom(), Name :: atom() | binary()).
|
||||
|
@ -43,6 +46,11 @@
|
|||
| {redirect, tuple()}
|
||||
| {error, Reason :: term()}.
|
||||
|
||||
-callback convert_certs(
|
||||
Dir :: file:filename_all(),
|
||||
config()
|
||||
) -> config().
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Callback Interface
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -66,6 +74,9 @@ destroy(Mod, State) ->
|
|||
login(Mod, Req, State) ->
|
||||
Mod:login(Req, State).
|
||||
|
||||
convert_certs(Mod, Dir, Config) ->
|
||||
Mod:convert_certs(Dir, Config).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% API
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -83,3 +94,23 @@ backends() ->
|
|||
ldap => emqx_dashboard_sso_ldap,
|
||||
saml => emqx_dashboard_sso_saml
|
||||
}.
|
||||
|
||||
format(Args) ->
|
||||
lists:foldl(fun combine/2, <<>>, Args).
|
||||
|
||||
combine(Arg, Bin) when is_binary(Arg) ->
|
||||
<<Bin/binary, Arg/binary>>;
|
||||
combine(Arg, Bin) when is_list(Arg) ->
|
||||
case io_lib:printable_unicode_list(Arg) of
|
||||
true ->
|
||||
ArgBin = unicode:characters_to_binary(Arg),
|
||||
<<Bin/binary, ArgBin/binary>>;
|
||||
_ ->
|
||||
generic_combine(Arg, Bin)
|
||||
end;
|
||||
combine(Arg, Bin) ->
|
||||
generic_combine(Arg, Bin).
|
||||
|
||||
generic_combine(Arg, Bin) ->
|
||||
Str = io_lib:format("~0p", [Arg]),
|
||||
erlang:iolist_to_binary([Bin, Str]).
|
||||
|
|
|
@ -33,13 +33,14 @@
|
|||
backend/2
|
||||
]).
|
||||
|
||||
-export([sso_parameters/1, login_reply/2]).
|
||||
-export([sso_parameters/1, login_meta/3]).
|
||||
|
||||
-define(REDIRECT, 'REDIRECT').
|
||||
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
|
||||
-define(BAD_REQUEST, 'BAD_REQUEST').
|
||||
-define(BACKEND_NOT_FOUND, 'BACKEND_NOT_FOUND').
|
||||
-define(TAGS, <<"Dashboard Single Sign-On">>).
|
||||
-define(MOD_KEY_PATH, [dashboard, sso]).
|
||||
|
||||
namespace() -> "dashboard_sso".
|
||||
|
||||
|
@ -132,69 +133,88 @@ schema("/sso/:backend") ->
|
|||
}.
|
||||
|
||||
fields(backend_status) ->
|
||||
emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()).
|
||||
emqx_dashboard_sso_schema:common_backend_schema(emqx_dashboard_sso:types()) ++
|
||||
[
|
||||
{running,
|
||||
mk(
|
||||
boolean(), #{
|
||||
desc => ?DESC(running)
|
||||
}
|
||||
)},
|
||||
{last_error,
|
||||
mk(
|
||||
binary(), #{
|
||||
desc => ?DESC(last_error)
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
running(get, _Request) ->
|
||||
SSO = emqx:get_config([dashboard_sso], #{}),
|
||||
{200,
|
||||
lists:filtermap(
|
||||
fun
|
||||
(#{backend := Backend, enable := true}) ->
|
||||
{true, Backend};
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
maps:values(SSO)
|
||||
)}.
|
||||
{200, emqx_dashboard_sso_manager:running()}.
|
||||
|
||||
login(post, #{bindings := #{backend := Backend}} = Request) ->
|
||||
login(post, #{bindings := #{backend := Backend}, body := Body} = Request) ->
|
||||
case emqx_dashboard_sso_manager:lookup_state(Backend) of
|
||||
undefined ->
|
||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||
State ->
|
||||
case emqx_dashboard_sso:login(provider(Backend), Request, State) of
|
||||
{ok, Role, Token} ->
|
||||
?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Request}),
|
||||
{200, login_reply(Role, Token)};
|
||||
?SLOG(info, #{
|
||||
msg => "dashboard_sso_login_successful",
|
||||
request => emqx_utils:redact(Request)
|
||||
}),
|
||||
Username = maps:get(<<"username">>, Body),
|
||||
{200, login_meta(Username, Role, Token)};
|
||||
{redirect, Redirect} ->
|
||||
?SLOG(info, #{msg => "dashboard_sso_login_redirect", request => Request}),
|
||||
?SLOG(info, #{
|
||||
msg => "dashboard_sso_login_redirect",
|
||||
request => emqx_utils:redact(Request)
|
||||
}),
|
||||
Redirect;
|
||||
{error, Reason} ->
|
||||
?SLOG(info, #{
|
||||
msg => "dashboard_sso_login_failed",
|
||||
request => Request,
|
||||
reason => Reason
|
||||
request => emqx_utils:redact(Request),
|
||||
reason => emqx_utils:redact(Reason)
|
||||
}),
|
||||
{401, #{code => ?BAD_USERNAME_OR_PWD, message => <<"Auth failed">>}}
|
||||
end
|
||||
end.
|
||||
|
||||
sso(get, _Request) ->
|
||||
SSO = emqx:get_config([dashboard_sso], #{}),
|
||||
SSO = emqx:get_config(?MOD_KEY_PATH, #{}),
|
||||
{200,
|
||||
lists:map(
|
||||
fun(Backend) ->
|
||||
maps:with([backend, enable], Backend)
|
||||
fun(#{backend := Backend, enable := Enable}) ->
|
||||
Status = emqx_dashboard_sso_manager:get_backend_status(Backend, Enable),
|
||||
Status#{
|
||||
backend => Backend,
|
||||
enable => Enable
|
||||
}
|
||||
end,
|
||||
maps:values(SSO)
|
||||
)}.
|
||||
|
||||
backend(get, #{bindings := #{backend := Type}}) ->
|
||||
case emqx:get_config([dashboard_sso, Type], undefined) of
|
||||
case emqx:get_config(?MOD_KEY_PATH ++ [Type], undefined) of
|
||||
undefined ->
|
||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||
Backend ->
|
||||
{200, to_json(Backend)}
|
||||
end;
|
||||
backend(put, #{bindings := #{backend := Backend}, body := Config}) ->
|
||||
?SLOG(info, #{msg => "Update SSO backend", backend => Backend, config => Config}),
|
||||
?SLOG(info, #{
|
||||
msg => "update_sso_backend",
|
||||
backend => Backend,
|
||||
config => emqx_utils:redact(Config)
|
||||
}),
|
||||
on_backend_update(Backend, Config, fun emqx_dashboard_sso_manager:update/2);
|
||||
backend(delete, #{bindings := #{backend := Backend}}) ->
|
||||
?SLOG(info, #{msg => "Delete SSO backend", backend => Backend}),
|
||||
?SLOG(info, #{msg => "delete_sso_backend", backend => Backend}),
|
||||
handle_backend_update_result(emqx_dashboard_sso_manager:delete(Backend), undefined).
|
||||
|
||||
sso_parameters(Params) ->
|
||||
|
@ -251,12 +271,12 @@ handle_backend_update_result(ok, _) ->
|
|||
204;
|
||||
handle_backend_update_result({error, not_exists}, _) ->
|
||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||
handle_backend_update_result({error, already_exists}, _) ->
|
||||
{400, #{code => ?BAD_REQUEST, message => <<"Backend already exists">>}};
|
||||
handle_backend_update_result({error, failed_to_load_metadata}, _) ->
|
||||
{400, #{code => ?BAD_REQUEST, message => <<"Failed to load metadata">>}};
|
||||
handle_backend_update_result({error, Reason}, _) when is_binary(Reason) ->
|
||||
{400, #{code => ?BAD_REQUEST, message => Reason}};
|
||||
handle_backend_update_result({error, Reason}, _) ->
|
||||
{400, #{code => ?BAD_REQUEST, message => Reason}}.
|
||||
{400, #{code => ?BAD_REQUEST, message => emqx_dashboard_sso:format(["Reason: ", Reason])}}.
|
||||
|
||||
to_json(Data) ->
|
||||
emqx_utils_maps:jsonable_map(
|
||||
|
@ -266,8 +286,9 @@ to_json(Data) ->
|
|||
end
|
||||
).
|
||||
|
||||
login_reply(Role, Token) ->
|
||||
login_meta(Username, Role, Token) ->
|
||||
#{
|
||||
username => Username,
|
||||
role => Role,
|
||||
token => Token,
|
||||
version => iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_dashboard_sso_cli).
|
||||
|
||||
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||
|
||||
-export([admins/1]).
|
||||
|
||||
-import(emqx_dashboard_cli, [bin/1, print_error/1]).
|
||||
|
||||
admins(["add", Username, Password]) ->
|
||||
admins(["add", Username, Password, ""]);
|
||||
admins(["add", Username, Password, Desc]) ->
|
||||
case emqx_dashboard_admin:add_user(bin(Username), bin(Password), ?ROLE_DEFAULT, bin(Desc)) of
|
||||
{ok, _} ->
|
||||
emqx_ctl:print("ok~n");
|
||||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end;
|
||||
admins(["add", Username, Password, Desc, Role]) ->
|
||||
case emqx_dashboard_admin:add_user(bin(Username), bin(Password), bin(Role), bin(Desc)) of
|
||||
{ok, _} ->
|
||||
emqx_ctl:print("ok~n");
|
||||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end;
|
||||
admins(["passwd", Username, Password]) ->
|
||||
case emqx_dashboard_admin:change_password(bin(Username), bin(Password)) of
|
||||
{ok, _} ->
|
||||
emqx_ctl:print("ok~n");
|
||||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end;
|
||||
admins(["del", Username]) ->
|
||||
delete_user(bin(Username));
|
||||
admins(["del", Username, BackendName]) ->
|
||||
case atom(BackendName) of
|
||||
{ok, ?BACKEND_LOCAL} ->
|
||||
delete_user(bin(Username));
|
||||
{ok, Backend} ->
|
||||
delete_user(?SSO_USERNAME(Backend, bin(Username)));
|
||||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end;
|
||||
admins(_) ->
|
||||
emqx_ctl:usage(
|
||||
[
|
||||
{"admins add <Username> <Password> <Description> <Role>", "Add dashboard user"},
|
||||
{"admins passwd <Username> <Password>", "Reset dashboard user password"},
|
||||
{"admins del <Username> <Backend>",
|
||||
"Delete dashboard user, <Backend> can be omitted, the default value is 'local'"}
|
||||
]
|
||||
).
|
||||
|
||||
atom(S) ->
|
||||
emqx_utils:safe_to_existing_atom(S).
|
||||
|
||||
delete_user(Username) ->
|
||||
case emqx_dashboard_admin:remove_user(Username) of
|
||||
{ok, _} ->
|
||||
emqx_ctl:print("ok~n");
|
||||
{error, Reason} ->
|
||||
print_error(Reason)
|
||||
end.
|
|
@ -22,7 +22,8 @@
|
|||
login/2,
|
||||
create/1,
|
||||
update/2,
|
||||
destroy/1
|
||||
destroy/1,
|
||||
convert_certs/2
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -86,19 +87,7 @@ destroy(#{resource_id := ResourceId}) ->
|
|||
|
||||
parse_config(Config0) ->
|
||||
Config = ensure_bind_password(Config0),
|
||||
State = lists:foldl(
|
||||
fun(Key, Acc) ->
|
||||
case maps:find(Key, Config) of
|
||||
{ok, Value} when is_binary(Value) ->
|
||||
Acc#{Key := erlang:binary_to_list(Value)};
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end,
|
||||
Config,
|
||||
[query_timeout]
|
||||
),
|
||||
{Config, State}.
|
||||
{Config, maps:with([query_timeout], Config0)}.
|
||||
|
||||
%% In this feature, the `bind_password` is fixed, so it should conceal from the swagger,
|
||||
%% but the connector still needs it, hence we should add it back here
|
||||
|
@ -163,3 +152,21 @@ ensure_user_exists(Username) ->
|
|||
Error
|
||||
end
|
||||
end.
|
||||
|
||||
convert_certs(Dir, Conf) ->
|
||||
case
|
||||
emqx_tls_lib:ensure_ssl_files(
|
||||
Dir, maps:get(<<"ssl">>, Conf, undefined)
|
||||
)
|
||||
of
|
||||
{ok, SSL} ->
|
||||
new_ssl_source(Conf, SSL);
|
||||
{error, Reason} ->
|
||||
?SLOG(error, Reason#{msg => "bad_ssl_config"}),
|
||||
throw({bad_ssl_config, Reason})
|
||||
end.
|
||||
|
||||
new_ssl_source(Source, undefined) ->
|
||||
Source;
|
||||
new_ssl_source(Source, SSL) ->
|
||||
Source#{<<"ssl">> => SSL}.
|
||||
|
|
|
@ -25,30 +25,39 @@
|
|||
-export([
|
||||
running/0,
|
||||
lookup_state/1,
|
||||
get_backend_status/2,
|
||||
make_resource_id/1,
|
||||
create_resource/3,
|
||||
update_resource/3,
|
||||
call/1
|
||||
update_resource/3
|
||||
]).
|
||||
|
||||
-export([
|
||||
update/2,
|
||||
delete/1,
|
||||
pre_config_update/3,
|
||||
post_config_update/5
|
||||
post_config_update/5,
|
||||
propagated_post_config_update/5
|
||||
]).
|
||||
|
||||
-import(emqx_dashboard_sso, [provider/1]).
|
||||
-import(emqx_dashboard_sso, [provider/1, format/1]).
|
||||
|
||||
-define(MOD_KEY_PATH, [dashboard_sso]).
|
||||
-define(MOD_TAB, emqx_dashboard_sso).
|
||||
-define(MOD_KEY_PATH, [dashboard, sso]).
|
||||
-define(MOD_KEY_PATH(Sub), [dashboard, sso, Sub]).
|
||||
-define(RESOURCE_GROUP, <<"emqx_dashboard_sso">>).
|
||||
-define(NO_ERROR, <<>>).
|
||||
-define(DEFAULT_RESOURCE_OPTS, #{
|
||||
start_after_created => false
|
||||
}).
|
||||
|
||||
-record(dashboard_sso, {
|
||||
-define(DEFAULT_START_OPTS, #{
|
||||
start_timeout => timer:seconds(30)
|
||||
}).
|
||||
|
||||
-record(?MOD_TAB, {
|
||||
backend :: atom(),
|
||||
state :: map()
|
||||
state :: undefined | map(),
|
||||
last_error = ?NO_ERROR :: term()
|
||||
}).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -58,26 +67,53 @@ start_link() ->
|
|||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
running() ->
|
||||
maps:fold(
|
||||
SSO = emqx:get_config(?MOD_KEY_PATH, #{}),
|
||||
lists:filtermap(
|
||||
fun
|
||||
(Type, #{enable := true}, Acc) ->
|
||||
[Type | Acc];
|
||||
(_Type, _Cfg, Acc) ->
|
||||
Acc
|
||||
(#{backend := Backend, enable := true}) ->
|
||||
case lookup(Backend) of
|
||||
undefined ->
|
||||
false;
|
||||
#?MOD_TAB{last_error = ?NO_ERROR} ->
|
||||
{true, Backend};
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
[],
|
||||
emqx:get_config([emqx_dashboard_sso])
|
||||
maps:values(SSO)
|
||||
).
|
||||
|
||||
get_backend_status(Backend, false) ->
|
||||
#{
|
||||
backend => Backend,
|
||||
enable => false,
|
||||
running => false,
|
||||
last_error => ?NO_ERROR
|
||||
};
|
||||
get_backend_status(Backend, _) ->
|
||||
case lookup(Backend) of
|
||||
undefined ->
|
||||
#{
|
||||
backend => Backend,
|
||||
enable => true,
|
||||
running => false,
|
||||
last_error => <<"Resource not found">>
|
||||
};
|
||||
Data ->
|
||||
maps:merge(#{backend => Backend, enable => true}, do_get_backend_status(Data))
|
||||
end.
|
||||
|
||||
update(Backend, Config) ->
|
||||
update_config(Backend, {?FUNCTION_NAME, Backend, Config}).
|
||||
delete(Backend) ->
|
||||
update_config(Backend, {?FUNCTION_NAME, Backend}).
|
||||
|
||||
lookup_state(Backend) ->
|
||||
case ets:lookup(dashboard_sso, Backend) of
|
||||
case ets:lookup(?MOD_TAB, Backend) of
|
||||
[Data] ->
|
||||
Data#dashboard_sso.state;
|
||||
Data#?MOD_TAB.state;
|
||||
[] ->
|
||||
undefined
|
||||
end.
|
||||
|
@ -102,31 +138,25 @@ update_resource(ResourceId, Module, Config) ->
|
|||
),
|
||||
start_resource_if_enabled(ResourceId, Result, Config).
|
||||
|
||||
call(Req) ->
|
||||
gen_server:call(?MODULE, Req).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% gen_server callbacks
|
||||
%%------------------------------------------------------------------------------
|
||||
init([]) ->
|
||||
process_flag(trap_exit, true),
|
||||
emqx_conf:add_handler(?MOD_KEY_PATH, ?MODULE),
|
||||
add_handler(),
|
||||
emqx_utils_ets:new(
|
||||
dashboard_sso,
|
||||
?MOD_TAB,
|
||||
[
|
||||
set,
|
||||
ordered_set,
|
||||
public,
|
||||
named_table,
|
||||
{keypos, #dashboard_sso.backend},
|
||||
{keypos, #?MOD_TAB.backend},
|
||||
{read_concurrency, true}
|
||||
]
|
||||
),
|
||||
start_backend_services(),
|
||||
{ok, #{}}.
|
||||
|
||||
handle_call({update_config, Req, NewConf}, _From, State) ->
|
||||
Result = on_config_update(Req, NewConf),
|
||||
{reply, Result, State};
|
||||
handle_call(_Request, _From, State) ->
|
||||
Reply = ok,
|
||||
{reply, Reply, State}.
|
||||
|
@ -138,7 +168,7 @@ handle_info(_Info, State) ->
|
|||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
emqx_conf:remove_handler(?MOD_KEY_PATH),
|
||||
remove_handler(),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
|
@ -151,22 +181,24 @@ format_status(_Opt, Status) ->
|
|||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
start_backend_services() ->
|
||||
Backends = emqx_conf:get([dashboard_sso], #{}),
|
||||
Backends = emqx_conf:get(?MOD_KEY_PATH, #{}),
|
||||
lists:foreach(
|
||||
fun({Backend, Config}) ->
|
||||
Provider = provider(Backend),
|
||||
case emqx_dashboard_sso:create(Provider, Config) of
|
||||
{ok, State} ->
|
||||
?SLOG(info, #{
|
||||
msg => "Start SSO backend successfully",
|
||||
msg => "start_sso_backend_successfully",
|
||||
backend => Backend
|
||||
}),
|
||||
ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State});
|
||||
update_state(Backend, State);
|
||||
{error, Reason} ->
|
||||
SafeReason = emqx_utils:redact(Reason),
|
||||
update_last_error(Backend, SafeReason),
|
||||
?SLOG(error, #{
|
||||
msg => "Start SSO backend failed",
|
||||
msg => "start_sso_backend_failed",
|
||||
backend => Backend,
|
||||
reason => Reason
|
||||
reason => SafeReason
|
||||
})
|
||||
end
|
||||
end,
|
||||
|
@ -174,96 +206,188 @@ start_backend_services() ->
|
|||
).
|
||||
|
||||
update_config(Backend, UpdateReq) ->
|
||||
case emqx_conf:update([dashboard_sso], UpdateReq, #{override_to => cluster}) of
|
||||
{ok, UpdateResult} ->
|
||||
#{post_config_update := #{?MODULE := Result}} = UpdateResult,
|
||||
?SLOG(info, #{
|
||||
msg => "Update SSO configuration successfully",
|
||||
backend => Backend,
|
||||
result => Result
|
||||
}),
|
||||
Result;
|
||||
%% we always make sure the valid configuration will update successfully,
|
||||
%% ignore the runtime error during its update
|
||||
case emqx_conf:update(?MOD_KEY_PATH(Backend), UpdateReq, #{override_to => cluster}) of
|
||||
{ok, _UpdateResult} ->
|
||||
case lookup(Backend) of
|
||||
undefined ->
|
||||
ok;
|
||||
#?MOD_TAB{state = State, last_error = ?NO_ERROR} ->
|
||||
{ok, State};
|
||||
Data ->
|
||||
{error, Data#?MOD_TAB.last_error}
|
||||
end;
|
||||
{error, Reason} = Error ->
|
||||
SafeReason = emqx_utils:redact(Reason),
|
||||
?SLOG(error, #{
|
||||
msg => "Update SSO configuration failed",
|
||||
msg => "update_sso_failed",
|
||||
backend => Backend,
|
||||
reason => Reason
|
||||
reason => SafeReason
|
||||
}),
|
||||
Error
|
||||
end.
|
||||
|
||||
pre_config_update(_Path, {update, Backend, Config}, OldConf) ->
|
||||
BackendBin = bin(Backend),
|
||||
{ok, OldConf#{BackendBin => Config}};
|
||||
pre_config_update(_Path, {delete, Backend}, OldConf) ->
|
||||
BackendBin = bin(Backend),
|
||||
case maps:find(BackendBin, OldConf) of
|
||||
error ->
|
||||
throw(not_exists);
|
||||
{ok, _} ->
|
||||
{ok, maps:remove(BackendBin, OldConf)}
|
||||
pre_config_update(_, {update, _Backend, Config}, _OldConf) ->
|
||||
{ok, maybe_write_certs(Config)};
|
||||
pre_config_update(_, {delete, _Backend}, undefined) ->
|
||||
throw(not_exists);
|
||||
pre_config_update(_, {delete, _Backend}, _OldConf) ->
|
||||
{ok, null}.
|
||||
|
||||
post_config_update(_, UpdateReq, NewConf, _OldConf, _AppEnvs) ->
|
||||
_ = on_config_update(UpdateReq, NewConf),
|
||||
ok.
|
||||
|
||||
propagated_post_config_update(
|
||||
?MOD_KEY_PATH(BackendBin) = Path, _UpdateReq, undefined, OldConf, AppEnvs
|
||||
) ->
|
||||
case atom(BackendBin) of
|
||||
{ok, Backend} ->
|
||||
post_config_update(Path, {delete, Backend}, undefined, OldConf, AppEnvs);
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
propagated_post_config_update(
|
||||
?MOD_KEY_PATH(BackendBin) = Path, _UpdateReq, NewConf, OldConf, AppEnvs
|
||||
) ->
|
||||
case atom(BackendBin) of
|
||||
{ok, Backend} ->
|
||||
post_config_update(Path, {update, Backend, undefined}, NewConf, OldConf, AppEnvs);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
post_config_update(_Path, UpdateReq, NewConf, _OldConf, _AppEnvs) ->
|
||||
Result = call({update_config, UpdateReq, NewConf}),
|
||||
{ok, Result}.
|
||||
|
||||
on_config_update({update, Backend, _Config}, NewConf) ->
|
||||
on_config_update({update, Backend, _RawConfig}, Config) ->
|
||||
Provider = provider(Backend),
|
||||
Config = maps:get(Backend, NewConf),
|
||||
case lookup(Backend) of
|
||||
undefined ->
|
||||
on_backend_updated(
|
||||
Backend,
|
||||
emqx_dashboard_sso:create(Provider, Config),
|
||||
fun(State) ->
|
||||
ets:insert(dashboard_sso, #dashboard_sso{backend = Backend, state = State})
|
||||
update_state(Backend, State)
|
||||
end
|
||||
);
|
||||
Data ->
|
||||
update_last_error(Backend, ?NO_ERROR),
|
||||
on_backend_updated(
|
||||
emqx_dashboard_sso:update(Provider, Config, Data#dashboard_sso.state),
|
||||
Backend,
|
||||
emqx_dashboard_sso:update(Provider, Config, Data#?MOD_TAB.state),
|
||||
fun(State) ->
|
||||
ets:insert(dashboard_sso, Data#dashboard_sso{state = State})
|
||||
update_state(Backend, State)
|
||||
end
|
||||
)
|
||||
end;
|
||||
on_config_update({delete, Backend}, _NewConf) ->
|
||||
case lookup(Backend) of
|
||||
undefined ->
|
||||
{error, not_exists};
|
||||
on_backend_updated(Backend, {error, not_exists}, undefined);
|
||||
Data ->
|
||||
Provider = provider(Backend),
|
||||
on_backend_updated(
|
||||
emqx_dashboard_sso:destroy(Provider, Data#dashboard_sso.state),
|
||||
Backend,
|
||||
emqx_dashboard_sso:destroy(Provider, Data#?MOD_TAB.state),
|
||||
fun() ->
|
||||
ets:delete(dashboard_sso, Backend)
|
||||
ets:delete(?MOD_TAB, Backend)
|
||||
end
|
||||
)
|
||||
end.
|
||||
|
||||
lookup(Backend) ->
|
||||
case ets:lookup(dashboard_sso, Backend) of
|
||||
case ets:lookup(?MOD_TAB, Backend) of
|
||||
[Data] ->
|
||||
Data;
|
||||
[] ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
start_resource_if_enabled(ResourceId, {ok, _} = Result, #{enable := true}) ->
|
||||
_ = emqx_resource:start(ResourceId),
|
||||
%% to avoid resource leakage the resource start will never affect the update result,
|
||||
%% so the resource_id will always be recorded
|
||||
start_resource_if_enabled(ResourceId, {ok, _} = Result, #{enable := true, backend := Backend}) ->
|
||||
case emqx_resource:start(ResourceId, ?DEFAULT_START_OPTS) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
SafeReason = emqx_utils:redact(Reason),
|
||||
?SLOG(error, #{
|
||||
msg => "start_backend_failed",
|
||||
resource_id => ResourceId,
|
||||
reason => SafeReason
|
||||
}),
|
||||
update_last_error(Backend, SafeReason),
|
||||
ok
|
||||
end,
|
||||
Result;
|
||||
start_resource_if_enabled(_ResourceId, Result, _Config) ->
|
||||
Result.
|
||||
|
||||
on_backend_updated({ok, State} = Ok, Fun) ->
|
||||
on_backend_updated(_Backend, {ok, State} = Ok, Fun) ->
|
||||
Fun(State),
|
||||
Ok;
|
||||
on_backend_updated(ok, Fun) ->
|
||||
on_backend_updated(_Backend, ok, Fun) ->
|
||||
Fun(),
|
||||
ok;
|
||||
on_backend_updated(Error, _) ->
|
||||
on_backend_updated(Backend, {error, Reason} = Error, _) ->
|
||||
update_last_error(Backend, Reason),
|
||||
Error.
|
||||
|
||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||
bin(L) when is_list(L) -> list_to_binary(L);
|
||||
bin(X) -> X.
|
||||
|
||||
atom(B) ->
|
||||
emqx_utils:safe_to_existing_atom(B).
|
||||
|
||||
add_handler() ->
|
||||
ok = emqx_conf:add_handler(?MOD_KEY_PATH('?'), ?MODULE).
|
||||
|
||||
remove_handler() ->
|
||||
ok = emqx_conf:remove_handler(?MOD_KEY_PATH('?')).
|
||||
|
||||
maybe_write_certs(#{<<"backend">> := Backend} = Conf) ->
|
||||
Dir = certs_path(Backend),
|
||||
Provider = provider(Backend),
|
||||
emqx_dashboard_sso:convert_certs(Provider, Dir, Conf).
|
||||
|
||||
certs_path(Backend) ->
|
||||
filename:join(["sso", Backend]).
|
||||
|
||||
update_state(Backend, State) ->
|
||||
Data = ensure_backend_data(Backend),
|
||||
ets:insert(?MOD_TAB, Data#?MOD_TAB{state = State}).
|
||||
|
||||
update_last_error(Backend, LastError) ->
|
||||
Data = ensure_backend_data(Backend),
|
||||
ets:insert(?MOD_TAB, Data#?MOD_TAB{last_error = LastError}).
|
||||
|
||||
ensure_backend_data(Backend) ->
|
||||
case ets:lookup(?MOD_TAB, Backend) of
|
||||
[Data] ->
|
||||
Data;
|
||||
[] ->
|
||||
#?MOD_TAB{backend = Backend}
|
||||
end.
|
||||
|
||||
do_get_backend_status(#?MOD_TAB{state = #{resource_id := ResourceId}}) ->
|
||||
case emqx_resource_manager:lookup(ResourceId) of
|
||||
{ok, _Group, #{status := connected}} ->
|
||||
#{running => true, last_error => ?NO_ERROR};
|
||||
{ok, _Group, #{status := Status}} ->
|
||||
#{
|
||||
running => false,
|
||||
last_error => format([<<"Resource not valid, status: ">>, Status])
|
||||
};
|
||||
{error, not_found} ->
|
||||
#{
|
||||
running => false,
|
||||
last_error => <<"Resource not found">>
|
||||
}
|
||||
end;
|
||||
do_get_backend_status(#?MOD_TAB{last_error = ?NO_ERROR}) ->
|
||||
#{running => true, last_error => ?NO_ERROR};
|
||||
do_get_backend_status(#?MOD_TAB{last_error = LastError}) ->
|
||||
#{
|
||||
running => false,
|
||||
last_error => format([LastError])
|
||||
}.
|
||||
|
|
|
@ -22,13 +22,21 @@
|
|||
-export([
|
||||
create/1,
|
||||
update/2,
|
||||
destroy/1
|
||||
destroy/1,
|
||||
convert_certs/2
|
||||
]).
|
||||
|
||||
-export([login/2, callback/2]).
|
||||
|
||||
-dialyzer({nowarn_function, do_create/1}).
|
||||
|
||||
-define(RESPHEADERS, #{
|
||||
<<"cache-control">> => <<"no-cache">>,
|
||||
<<"pragma">> => <<"no-cache">>,
|
||||
<<"content-type">> => <<"text/plain">>
|
||||
}).
|
||||
-define(REDIRECT_BODY, <<"Redirecting...">>).
|
||||
|
||||
-define(DIR, <<"saml_sp_certs">>).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -93,9 +101,11 @@ desc(_) ->
|
|||
%% APIs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
create(#{enable := false} = _Config) ->
|
||||
{ok, undefined};
|
||||
create(#{sp_sign_request := true} = Config) ->
|
||||
try
|
||||
do_create(ensure_cert_and_key(Config))
|
||||
do_create(Config)
|
||||
catch
|
||||
Kind:Error ->
|
||||
Msg = failed_to_ensure_cert_and_key,
|
||||
|
@ -103,7 +113,70 @@ create(#{sp_sign_request := true} = Config) ->
|
|||
{error, Msg}
|
||||
end;
|
||||
create(#{sp_sign_request := false} = Config) ->
|
||||
do_create(Config#{key => undefined, certificate => undefined}).
|
||||
do_create(Config#{sp_private_key => undefined, sp_public_key => undefined}).
|
||||
|
||||
update(Config0, State) ->
|
||||
destroy(State),
|
||||
create(Config0).
|
||||
|
||||
destroy(_State) ->
|
||||
_ = file:del_dir_r(emqx_tls_lib:pem_dir(?DIR)),
|
||||
_ = application:stop(esaml),
|
||||
ok.
|
||||
|
||||
login(
|
||||
#{headers := Headers} = _Req,
|
||||
#{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State
|
||||
) ->
|
||||
SignedXml = esaml_sp:generate_authn_request(IDP, SP),
|
||||
Target = esaml_binding:encode_http_redirect(IDP, SignedXml, <<>>),
|
||||
Redirect =
|
||||
case is_msie(Headers) of
|
||||
true ->
|
||||
Html = esaml_binding:encode_http_post(IDP, SignedXml, <<>>),
|
||||
{200, ?RESPHEADERS, Html};
|
||||
false ->
|
||||
{302, ?RESPHEADERS#{<<"location">> => Target}, ?REDIRECT_BODY}
|
||||
end,
|
||||
{redirect, Redirect}.
|
||||
|
||||
callback(_Req = #{body := Body}, #{sp := SP, dashboard_addr := DashboardAddr} = _State) ->
|
||||
case do_validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Body) of
|
||||
{ok, Assertion, _RelayState} ->
|
||||
Subject = Assertion#esaml_assertion.subject,
|
||||
Username = iolist_to_binary(Subject#esaml_subject.name),
|
||||
gen_redirect_response(DashboardAddr, Username);
|
||||
{error, Reason0} ->
|
||||
Reason = [
|
||||
"Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0])
|
||||
],
|
||||
{error, iolist_to_binary(Reason)}
|
||||
end.
|
||||
|
||||
convert_certs(
|
||||
Dir,
|
||||
#{<<"sp_sign_request">> := true, <<"sp_public_key">> := Cert, <<"sp_private_key">> := Key} =
|
||||
Conf
|
||||
) ->
|
||||
case
|
||||
emqx_tls_lib:ensure_ssl_files(
|
||||
Dir, #{enable => ture, certfile => Cert, keyfile => Key}, #{}
|
||||
)
|
||||
of
|
||||
{ok, #{certfile := CertPath, keyfile := KeyPath}} ->
|
||||
Conf#{<<"sp_public_key">> => bin(CertPath), <<"sp_private_key">> => bin(KeyPath)};
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{msg => "failed_to_save_sp_sign_keys", reason => Reason}),
|
||||
throw("Failed to save sp signing key(s)")
|
||||
end;
|
||||
convert_certs(_Dir, Conf) ->
|
||||
Conf.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
bin(X) -> iolist_to_binary(X).
|
||||
|
||||
do_create(
|
||||
#{
|
||||
|
@ -145,46 +218,6 @@ do_create(
|
|||
{error, Reason}
|
||||
end.
|
||||
|
||||
update(Config0, State) ->
|
||||
destroy(State),
|
||||
create(Config0).
|
||||
|
||||
destroy(_State) ->
|
||||
_ = file:del_dir_r(emqx_tls_lib:pem_dir(?DIR)),
|
||||
_ = application:stop(esaml),
|
||||
ok.
|
||||
|
||||
login(
|
||||
#{headers := Headers} = _Req,
|
||||
#{sp := SP, idp_meta := #esaml_idp_metadata{login_location = IDP}} = _State
|
||||
) ->
|
||||
SignedXml = esaml_sp:generate_authn_request(IDP, SP),
|
||||
Target = esaml_binding:encode_http_redirect(IDP, SignedXml, <<>>),
|
||||
RespHeaders = #{<<"Cache-Control">> => <<"no-cache">>, <<"Pragma">> => <<"no-cache">>},
|
||||
Redirect =
|
||||
case is_msie(Headers) of
|
||||
true ->
|
||||
Html = esaml_binding:encode_http_post(IDP, SignedXml, <<>>),
|
||||
{200, RespHeaders, Html};
|
||||
false ->
|
||||
RespHeaders1 = RespHeaders#{<<"Location">> => Target},
|
||||
{302, RespHeaders1, <<"Redirecting...">>}
|
||||
end,
|
||||
{redirect, Redirect}.
|
||||
|
||||
callback(_Req = #{body := Body}, #{sp := SP} = _State) ->
|
||||
case do_validate_assertion(SP, fun esaml_util:check_dupe_ets/2, Body) of
|
||||
{ok, Assertion, _RelayState} ->
|
||||
Subject = Assertion#esaml_assertion.subject,
|
||||
Username = iolist_to_binary(Subject#esaml_subject.name),
|
||||
ensure_user_exists(Username);
|
||||
{error, Reason0} ->
|
||||
Reason = [
|
||||
"Access denied, assertion failed validation:\n", io_lib:format("~p\n", [Reason0])
|
||||
],
|
||||
{error, iolist_to_binary(Reason)}
|
||||
end.
|
||||
|
||||
do_validate_assertion(SP, DuplicateFun, Body) ->
|
||||
PostVals = cow_qs:parse_qs(Body),
|
||||
SAMLEncoding = proplists:get_value(<<"SAMLEncoding">>, PostVals),
|
||||
|
@ -200,30 +233,18 @@ do_validate_assertion(SP, DuplicateFun, Body) ->
|
|||
end
|
||||
end.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
ensure_cert_and_key(#{sp_public_key := Cert, sp_private_key := Key} = Config) ->
|
||||
case
|
||||
emqx_tls_lib:ensure_ssl_files(
|
||||
?DIR, #{enable => ture, certfile => Cert, keyfile => Key}, #{}
|
||||
)
|
||||
of
|
||||
{ok, #{certfile := CertPath, keyfile := KeyPath} = _NSSL} ->
|
||||
Config#{sp_public_key => CertPath, sp_private_key => KeyPath};
|
||||
{error, #{which_options := KeyPath}} ->
|
||||
error({missing_key, lists:flatten(KeyPath)})
|
||||
gen_redirect_response(DashboardAddr, Username) ->
|
||||
case ensure_user_exists(Username) of
|
||||
{ok, Role, Token} ->
|
||||
Target = login_redirect_target(DashboardAddr, Username, Role, Token),
|
||||
{redirect, {302, ?RESPHEADERS#{<<"location">> => Target}, ?REDIRECT_BODY}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
maybe_load_cert_or_key(undefined, _) ->
|
||||
undefined;
|
||||
maybe_load_cert_or_key(Path, Func) ->
|
||||
Func(Path).
|
||||
|
||||
is_msie(Headers) ->
|
||||
UA = maps:get(<<"user-agent">>, Headers, <<"">>),
|
||||
not (binary:match(UA, <<"MSIE">>) =:= nomatch).
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
%% TODO: unify with emqx_dashboard_sso_manager:ensure_user_exists/1
|
||||
ensure_user_exists(Username) ->
|
||||
|
@ -238,3 +259,19 @@ ensure_user_exists(Username) ->
|
|||
Error
|
||||
end
|
||||
end.
|
||||
|
||||
maybe_load_cert_or_key(undefined, _) ->
|
||||
undefined;
|
||||
maybe_load_cert_or_key(Path, Func) ->
|
||||
Func(Path).
|
||||
|
||||
is_msie(Headers) ->
|
||||
UA = maps:get(<<"user-agent">>, Headers, <<"">>),
|
||||
not (binary:match(UA, <<"MSIE">>) =:= nomatch).
|
||||
|
||||
login_redirect_target(DashboardAddr, Username, Role, Token) ->
|
||||
LoginMeta = emqx_dashboard_sso_api:login_meta(Username, Role, Token),
|
||||
<<DashboardAddr/binary, "/?login_meta=", (base64_login_meta(LoginMeta))/binary>>.
|
||||
|
||||
base64_login_meta(LoginMeta) ->
|
||||
base64:encode(emqx_utils_json:encode(LoginMeta)).
|
||||
|
|
|
@ -82,22 +82,20 @@ schema("/sso/saml/metadata") ->
|
|||
|
||||
sp_saml_metadata(get, _Req) ->
|
||||
case emqx_dashboard_sso_manager:lookup_state(saml) of
|
||||
undefined ->
|
||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||
#{sp := SP} = _State ->
|
||||
#{enable := true, sp := SP} = _State ->
|
||||
SignedXml = esaml_sp:generate_metadata(SP),
|
||||
Metadata = xmerl:export([SignedXml], xmerl_xml),
|
||||
{200, #{<<"Content-Type">> => <<"text/xml">>}, erlang:iolist_to_binary(Metadata)}
|
||||
{200, #{<<"content-type">> => <<"text/xml">>}, erlang:iolist_to_binary(Metadata)};
|
||||
_ ->
|
||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}
|
||||
end.
|
||||
|
||||
sp_saml_callback(post, Req) ->
|
||||
case emqx_dashboard_sso_manager:lookup_state(saml) of
|
||||
undefined ->
|
||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}};
|
||||
State ->
|
||||
State = #{enable := true} ->
|
||||
case (provider(saml)):callback(Req, State) of
|
||||
{ok, Role, Token} ->
|
||||
{200, emqx_dashboard_sso_api:login_reply(Role, Token)};
|
||||
{redirect, Redirect} ->
|
||||
Redirect;
|
||||
{error, Reason} ->
|
||||
?SLOG(info, #{
|
||||
msg => "dashboard_saml_sso_login_failed",
|
||||
|
@ -105,7 +103,9 @@ sp_saml_callback(post, Req) ->
|
|||
reason => Reason
|
||||
}),
|
||||
{403, #{code => <<"UNAUTHORIZED">>, message => Reason}}
|
||||
end
|
||||
end;
|
||||
_ ->
|
||||
{404, #{code => ?BACKEND_NOT_FOUND, message => <<"Backend not found">>}}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -8,33 +8,32 @@
|
|||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
%% Hocon
|
||||
-export([namespace/0, roots/0, fields/1, tags/0, desc/1]).
|
||||
-export([fields/1, desc/1]).
|
||||
|
||||
-export([
|
||||
common_backend_schema/1,
|
||||
backend_schema/1,
|
||||
username_password_schema/0
|
||||
]).
|
||||
|
||||
-import(hoconsc, [ref/2, mk/2, enum/1]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
%%------------------------------------------------------------------------------
|
||||
namespace() -> dashboard_sso.
|
||||
|
||||
tags() ->
|
||||
[<<"Dashboard Single Sign-On">>].
|
||||
|
||||
roots() -> [dashboard_sso].
|
||||
|
||||
fields(dashboard_sso) ->
|
||||
fields(sso) ->
|
||||
lists:map(
|
||||
fun({Type, Module}) ->
|
||||
{Type, mk(emqx_dashboard_sso:hocon_ref(Module), #{required => {false, recursively}})}
|
||||
{Type,
|
||||
mk(
|
||||
emqx_dashboard_sso:hocon_ref(Module),
|
||||
#{required => {false, recursively}}
|
||||
)}
|
||||
end,
|
||||
maps:to_list(emqx_dashboard_sso:backends())
|
||||
).
|
||||
|
||||
desc(dashboard_sso) ->
|
||||
desc(sso) ->
|
||||
"Dashboard Single Sign-On";
|
||||
desc(_) ->
|
||||
undefined.
|
||||
|
|
|
@ -8,28 +8,36 @@
|
|||
-compile(export_all).
|
||||
|
||||
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(LDAP_HOST, "ldap").
|
||||
-define(LDAP_DEFAULT_PORT, 389).
|
||||
-define(LDAP_USER, <<"mqttuser0001">>).
|
||||
-define(LDAP_USER_PASSWORD, <<"mqttuser0001">>).
|
||||
|
||||
-define(MOD_TAB, emqx_dashboard_sso).
|
||||
-define(MOD_KEY_PATH, [dashboard, sso, ldap]).
|
||||
-define(RESOURCE_GROUP, <<"emqx_dashboard_sso">>).
|
||||
|
||||
-import(emqx_mgmt_api_test_util, [request/2, request/3, uri/1, request_api/3]).
|
||||
|
||||
all() ->
|
||||
[
|
||||
t_bad_create,
|
||||
t_create,
|
||||
t_update,
|
||||
t_get,
|
||||
t_login_with_bad,
|
||||
t_first_login,
|
||||
t_next_login,
|
||||
t_bad_update,
|
||||
t_delete
|
||||
].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
_ = application:load(emqx_conf),
|
||||
emqx_config:save_schema_mod_and_names(emqx_dashboard_sso_schema),
|
||||
emqx_config:save_schema_mod_and_names(emqx_dashboard_schema),
|
||||
emqx_mgmt_api_test_util:init_suite([emqx_dashboard, emqx_dashboard_sso]),
|
||||
Config.
|
||||
|
||||
|
@ -53,11 +61,49 @@ end_per_testcase(Case, _) ->
|
|||
end,
|
||||
ok.
|
||||
|
||||
t_bad_create(_) ->
|
||||
Path = uri(["sso", "ldap"]),
|
||||
?assertMatch(
|
||||
{ok, 400, _},
|
||||
request(
|
||||
put,
|
||||
Path,
|
||||
ldap_config(#{
|
||||
<<"username">> => <<"invalid">>,
|
||||
<<"enable">> => true,
|
||||
<<"request_timeout">> => <<"1s">>
|
||||
})
|
||||
)
|
||||
),
|
||||
?assertMatch(#{backend := ldap}, emqx:get_config(?MOD_KEY_PATH, undefined)),
|
||||
check_running([]),
|
||||
?assertMatch(
|
||||
[#{backend := <<"ldap">>, enable := true, running := false, last_error := _}], get_sso()
|
||||
),
|
||||
|
||||
emqx_dashboard_sso_manager:delete(ldap),
|
||||
|
||||
?retry(
|
||||
_Interval = 500,
|
||||
_NAttempts = 10,
|
||||
?assertMatch([], emqx_resource_manager:list_group(?RESOURCE_GROUP))
|
||||
),
|
||||
ok.
|
||||
|
||||
t_create(_) ->
|
||||
check_running([]),
|
||||
Path = uri(["sso", "ldap"]),
|
||||
{ok, 200, Result} = request(put, Path, ldap_config()),
|
||||
check_running([]),
|
||||
|
||||
?assertMatch(#{backend := ldap}, emqx:get_config(?MOD_KEY_PATH, undefined)),
|
||||
?assertMatch([_], ets:tab2list(?MOD_TAB)),
|
||||
?retry(
|
||||
_Interval = 500,
|
||||
_NAttempts = 10,
|
||||
?assertMatch([_], emqx_resource_manager:list_group(?RESOURCE_GROUP))
|
||||
),
|
||||
|
||||
?assertMatch(#{backend := <<"ldap">>, enable := false}, decode_json(Result)),
|
||||
?assertMatch([#{backend := <<"ldap">>, enable := false}], get_sso()),
|
||||
?assertNotEqual(undefined, emqx_dashboard_sso_manager:lookup_state(ldap)),
|
||||
|
@ -119,6 +165,28 @@ t_next_login(_) ->
|
|||
?assertMatch(#{license := _, token := _}, decode_json(Result)),
|
||||
ok.
|
||||
|
||||
t_bad_update(_) ->
|
||||
Path = uri(["sso", "ldap"]),
|
||||
?assertMatch(
|
||||
{ok, 400, _},
|
||||
request(
|
||||
put,
|
||||
Path,
|
||||
ldap_config(#{
|
||||
<<"username">> => <<"invalid">>,
|
||||
<<"enable">> => true,
|
||||
<<"request_timeout">> => <<"1s">>
|
||||
})
|
||||
)
|
||||
),
|
||||
?assertMatch(#{backend := ldap}, emqx:get_config(?MOD_KEY_PATH, undefined)),
|
||||
check_running([]),
|
||||
?assertMatch(
|
||||
[#{backend := <<"ldap">>, enable := true, running := false, last_error := _}], get_sso()
|
||||
),
|
||||
|
||||
ok.
|
||||
|
||||
t_delete(_) ->
|
||||
Path = uri(["sso", "ldap"]),
|
||||
?assertMatch({ok, 204, _}, request(delete, Path)),
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
|
||||
-behaviour(hocon_schema).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
|
||||
-export([namespace/0, roots/0, fields/1, translations/0, translation/1, desc/1, validations/0]).
|
||||
|
||||
-define(EE_SCHEMA_MODULES, [
|
||||
emqx_license_schema,
|
||||
emqx_schema_registry_schema,
|
||||
emqx_ft_schema,
|
||||
emqx_dashboard_sso_schema
|
||||
emqx_ft_schema
|
||||
]).
|
||||
|
||||
namespace() ->
|
||||
|
@ -23,6 +25,61 @@ roots() ->
|
|||
|
||||
fields("node") ->
|
||||
redefine_node(emqx_conf_schema:fields("node"));
|
||||
fields("log") ->
|
||||
redefine_log(emqx_conf_schema:fields("log"));
|
||||
fields("log_audit_handler") ->
|
||||
CommonConfs = emqx_conf_schema:log_handler_common_confs(file, #{}),
|
||||
CommonConfs1 = lists:filter(
|
||||
fun({Key, _}) ->
|
||||
not lists:member(Key, ["level", "formatter"])
|
||||
end,
|
||||
CommonConfs
|
||||
),
|
||||
[
|
||||
{"level",
|
||||
hoconsc:mk(
|
||||
emqx_conf_schema:log_level(),
|
||||
#{
|
||||
default => info,
|
||||
desc => ?DESC(emqx_conf_schema, "audit_handler_level"),
|
||||
importance => ?IMPORTANCE_HIDDEN
|
||||
}
|
||||
)},
|
||||
|
||||
{"path",
|
||||
hoconsc:mk(
|
||||
emqx_conf_schema:file(),
|
||||
#{
|
||||
desc => ?DESC(emqx_conf_schema, "audit_file_handler_path"),
|
||||
default => <<"${EMQX_LOG_DIR}/audit.log">>,
|
||||
importance => ?IMPORTANCE_HIGH,
|
||||
converter => fun(Path, Opts) ->
|
||||
emqx_schema:naive_env_interpolation(
|
||||
emqx_conf_schema:ensure_unicode_path(Path, Opts)
|
||||
)
|
||||
end
|
||||
}
|
||||
)},
|
||||
{"rotation_count",
|
||||
hoconsc:mk(
|
||||
range(1, 128),
|
||||
#{
|
||||
default => 10,
|
||||
converter => fun emqx_conf_schema:convert_rotation/2,
|
||||
desc => ?DESC(emqx_conf_schema, "log_rotation_count"),
|
||||
importance => ?IMPORTANCE_MEDIUM
|
||||
}
|
||||
)},
|
||||
{"rotation_size",
|
||||
hoconsc:mk(
|
||||
hoconsc:union([infinity, emqx_schema:bytesize()]),
|
||||
#{
|
||||
default => <<"50MB">>,
|
||||
desc => ?DESC(emqx_conf_schema, "log_file_handler_max_size"),
|
||||
importance => ?IMPORTANCE_MEDIUM
|
||||
}
|
||||
)}
|
||||
] ++ CommonConfs1;
|
||||
fields(Name) ->
|
||||
ee_delegate(fields, ?EE_SCHEMA_MODULES, Name).
|
||||
|
||||
|
@ -32,6 +89,8 @@ translations() ->
|
|||
translation(Name) ->
|
||||
emqx_conf_schema:translation(Name).
|
||||
|
||||
desc("log_audit_handler") ->
|
||||
?DESC(emqx_conf_schema, "desc_audit_log_handler");
|
||||
desc(Name) ->
|
||||
ee_delegate(desc, ?EE_SCHEMA_MODULES, Name).
|
||||
|
||||
|
@ -61,13 +120,20 @@ ee_delegate(Method, [], Name) ->
|
|||
apply(emqx_conf_schema, Method, [Name]).
|
||||
|
||||
redefine_roots(Roots) ->
|
||||
Overrides = [{"node", #{type => hoconsc:ref(?MODULE, "node")}}],
|
||||
Overrides = [
|
||||
{"node", #{type => hoconsc:ref(?MODULE, "node")}},
|
||||
{"log", #{type => hoconsc:ref(?MODULE, "log")}}
|
||||
],
|
||||
override(Roots, Overrides).
|
||||
|
||||
redefine_node(Fields) ->
|
||||
Overrides = [],
|
||||
override(Fields, Overrides).
|
||||
|
||||
redefine_log(Fields) ->
|
||||
Overrides = [],
|
||||
override(Fields, Overrides) ++ audit_log_conf().
|
||||
|
||||
override(Fields, []) ->
|
||||
Fields;
|
||||
override(Fields, [{Name, Override} | More]) ->
|
||||
|
@ -82,3 +148,19 @@ find_schema(Name, Fields) ->
|
|||
|
||||
replace_schema(Name, Schema, Fields) ->
|
||||
lists:keyreplace(Name, 1, Fields, {Name, Schema}).
|
||||
|
||||
audit_log_conf() ->
|
||||
[
|
||||
{"audit",
|
||||
hoconsc:mk(
|
||||
hoconsc:ref(?MODULE, "log_audit_handler"),
|
||||
#{
|
||||
%% note: we need to keep the descriptions associated with
|
||||
%% `emqx_conf_schema' module hocon i18n file because that's what
|
||||
%% `emqx_conf:gen_config_md' seems to expect.
|
||||
desc => ?DESC(emqx_conf_schema, "log_audit_handler"),
|
||||
importance => ?IMPORTANCE_HIGH,
|
||||
default => #{<<"enable">> => true, <<"level">> => <<"info">>}
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
|
|
@ -13,6 +13,25 @@
|
|||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
init_per_testcase(t_audit_log_conf, Config) ->
|
||||
Apps = emqx_cth_suite:start(
|
||||
[
|
||||
emqx_enterprise,
|
||||
{emqx_conf, #{schema_mod => emqx_enterprise_schema}}
|
||||
],
|
||||
#{work_dir => emqx_cth_suite:work_dir(Config)}
|
||||
),
|
||||
[{apps, Apps} | Config];
|
||||
init_per_testcase(_TestCase, Config) ->
|
||||
Config.
|
||||
|
||||
end_per_testcase(t_audit_log_conf, Config) ->
|
||||
Apps = ?config(apps, Config),
|
||||
ok = emqx_cth_suite:stop(Apps),
|
||||
ok;
|
||||
end_per_testcase(_TestCase, _Config) ->
|
||||
ok.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Tests
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -50,3 +69,36 @@ t_translations(_Config) ->
|
|||
emqx_conf_schema:translation(Root),
|
||||
emqx_enterprise_schema:translation(Root)
|
||||
).
|
||||
|
||||
t_audit_log_conf(_Config) ->
|
||||
FileExpect = #{
|
||||
<<"enable">> => true,
|
||||
<<"formatter">> => <<"text">>,
|
||||
<<"level">> => <<"warning">>,
|
||||
<<"rotation_count">> => 10,
|
||||
<<"rotation_size">> => <<"50MB">>,
|
||||
<<"time_offset">> => <<"system">>,
|
||||
<<"path">> => <<"log/emqx.log">>
|
||||
},
|
||||
ExpectLog1 = #{
|
||||
<<"console">> =>
|
||||
#{
|
||||
<<"enable">> => false,
|
||||
<<"formatter">> => <<"text">>,
|
||||
<<"level">> => <<"warning">>,
|
||||
<<"time_offset">> => <<"system">>
|
||||
},
|
||||
<<"file">> =>
|
||||
#{<<"default">> => FileExpect},
|
||||
<<"audit">> =>
|
||||
#{
|
||||
<<"enable">> => true,
|
||||
<<"level">> => <<"info">>,
|
||||
<<"path">> => <<"log/audit.log">>,
|
||||
<<"rotation_count">> => 10,
|
||||
<<"rotation_size">> => <<"50MB">>,
|
||||
<<"time_offset">> => <<"system">>
|
||||
}
|
||||
},
|
||||
?assertEqual(ExpectLog1, emqx_conf:get_raw([<<"log">>])),
|
||||
ok.
|
||||
|
|
|
@ -16,3 +16,38 @@ doc_gen_test() ->
|
|||
ok = emqx_conf:dump_schema(Dir, emqx_enterprise_schema)
|
||||
end
|
||||
}.
|
||||
|
||||
audit_log_test() ->
|
||||
ensure_acl_conf(),
|
||||
Conf0 = <<"node {cookie = aaa, data_dir = \"/tmp\"}">>,
|
||||
{ok, ConfMap0} = hocon:binary(Conf0, #{format => richmap}),
|
||||
ConfList = hocon_tconf:generate(emqx_enterprise_schema, ConfMap0),
|
||||
Kernel = proplists:get_value(kernel, ConfList),
|
||||
Loggers = proplists:get_value(logger, Kernel),
|
||||
FileHandlers = lists:filter(fun(L) -> element(3, L) =:= logger_disk_log_h end, Loggers),
|
||||
AuditHandler = lists:keyfind(emqx_audit, 2, FileHandlers),
|
||||
%% default is enable and log level is info.
|
||||
?assertMatch(
|
||||
{handler, emqx_audit, logger_disk_log_h, #{
|
||||
config := #{
|
||||
type := wrap,
|
||||
file := "log/audit.log",
|
||||
max_no_bytes := _,
|
||||
max_no_files := _
|
||||
},
|
||||
filesync_repeat_interval := no_repeat,
|
||||
filters := [{filter_audit, {_, stop}}],
|
||||
formatter := _,
|
||||
level := info
|
||||
}},
|
||||
AuditHandler
|
||||
),
|
||||
ok.
|
||||
|
||||
ensure_acl_conf() ->
|
||||
File = emqx_schema:naive_env_interpolation(<<"${EMQX_ETC_DIR}/acl.conf">>),
|
||||
ok = filelib:ensure_dir(filename:dirname(File)),
|
||||
case filelib:is_regular(File) of
|
||||
true -> ok;
|
||||
false -> file:write_file(File, <<"">>)
|
||||
end.
|
||||
|
|
|
@ -74,7 +74,7 @@ fields(config) ->
|
|||
{request_timeout,
|
||||
?HOCON(emqx_schema:timeout_duration_ms(), #{
|
||||
desc => ?DESC(request_timeout),
|
||||
default => <<"5s">>
|
||||
default => <<"10s">>
|
||||
})},
|
||||
{ssl,
|
||||
?HOCON(?R_REF(?MODULE, ssl), #{
|
||||
|
@ -158,7 +158,7 @@ on_start(
|
|||
{error, Reason} ->
|
||||
?tp(
|
||||
ldap_connector_start_failed,
|
||||
#{error => Reason}
|
||||
#{error => emqx_utils:redact(Reason)}
|
||||
),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
@ -248,7 +248,7 @@ do_ldap_query(
|
|||
SearchOptions,
|
||||
#{pool_name := PoolName} = State
|
||||
) ->
|
||||
LogMeta = #{connector => InstId, search => SearchOptions, state => State},
|
||||
LogMeta = #{connector => InstId, search => SearchOptions, state => emqx_utils:redact(State)},
|
||||
?TRACE("QUERY", "ldap_connector_received", LogMeta),
|
||||
case
|
||||
ecpool:pick_and_do(
|
||||
|
@ -268,7 +268,10 @@ do_ldap_query(
|
|||
{error, Reason} ->
|
||||
?SLOG(
|
||||
error,
|
||||
LogMeta#{msg => "ldap_connector_do_query_failed", reason => Reason}
|
||||
LogMeta#{
|
||||
msg => "ldap_connector_do_query_failed",
|
||||
reason => emqx_utils:redact(Reason)
|
||||
}
|
||||
),
|
||||
{error, {unrecoverable_error, Reason}}
|
||||
end.
|
||||
|
|
|
@ -91,14 +91,14 @@ refs() ->
|
|||
create(_AuthenticatorID, Config) ->
|
||||
do_create(?MODULE, Config).
|
||||
|
||||
do_create(Module, Config0) ->
|
||||
do_create(Module, Config) ->
|
||||
ResourceId = emqx_authn_utils:make_resource_id(Module),
|
||||
{Config, State} = parse_config(Config0),
|
||||
State = parse_config(Config),
|
||||
{ok, _Data} = emqx_authn_utils:create_resource(ResourceId, emqx_ldap, Config),
|
||||
{ok, State#{resource_id => ResourceId}}.
|
||||
|
||||
update(Config0, #{resource_id := ResourceId} = _State) ->
|
||||
{Config, NState} = parse_config(Config0),
|
||||
update(Config, #{resource_id := ResourceId} = _State) ->
|
||||
NState = parse_config(Config),
|
||||
case emqx_authn_utils:update_resource(emqx_ldap, Config, ResourceId) of
|
||||
{error, Reason} ->
|
||||
error({load_config_error, Reason});
|
||||
|
@ -143,19 +143,7 @@ authenticate(
|
|||
end.
|
||||
|
||||
parse_config(Config) ->
|
||||
State = lists:foldl(
|
||||
fun(Key, Acc) ->
|
||||
case maps:find(Key, Config) of
|
||||
{ok, Value} when is_binary(Value) ->
|
||||
Acc#{Key := erlang:binary_to_list(Value)};
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end,
|
||||
Config,
|
||||
[password_attribute, is_superuser_attribute, query_timeout]
|
||||
),
|
||||
{Config, State}.
|
||||
maps:with([query_timeout, password_attribute, is_superuser_attribute], Config).
|
||||
|
||||
%% To compatible v4.x
|
||||
is_enabled(Password, #eldap_entry{attributes = Attributes} = Entry, State) ->
|
||||
|
|
|
@ -116,7 +116,7 @@ authorize(
|
|||
{error, Reason} ->
|
||||
?SLOG(error, #{
|
||||
msg => "query_ldap_error",
|
||||
reason => Reason,
|
||||
reason => emqx_utils:redact(Reason),
|
||||
resource_id => ResourceID
|
||||
}),
|
||||
nomatch
|
||||
|
@ -134,21 +134,10 @@ do_authorize(_Action, _Topic, [], _Entry) ->
|
|||
nomatch.
|
||||
|
||||
new_annotations(Init, Source) ->
|
||||
lists:foldl(
|
||||
fun(Attr, Acc) ->
|
||||
Acc#{
|
||||
Attr =>
|
||||
case maps:get(Attr, Source) of
|
||||
Value when is_binary(Value) ->
|
||||
erlang:binary_to_list(Value);
|
||||
Value ->
|
||||
Value
|
||||
end
|
||||
}
|
||||
end,
|
||||
Init,
|
||||
[publish_attribute, subscribe_attribute, all_attribute]
|
||||
).
|
||||
State = maps:with(
|
||||
[query_timeout, publish_attribute, subscribe_attribute, all_attribute], Source
|
||||
),
|
||||
maps:merge(Init, State).
|
||||
|
||||
select_attrs(#{action_type := publish}, #{publish_attribute := Pub, all_attribute := All}) ->
|
||||
[Pub, All];
|
||||
|
|
|
@ -61,7 +61,7 @@ on_query(
|
|||
{bind, Data},
|
||||
#{
|
||||
base_tokens := DNTks,
|
||||
bind_password_tokens := PWTks,
|
||||
bind_password := PWTks,
|
||||
bind_pool_name := PoolName
|
||||
} = State
|
||||
) ->
|
||||
|
@ -86,7 +86,7 @@ on_query(
|
|||
{error, Reason} ->
|
||||
?SLOG(
|
||||
error,
|
||||
LogMeta#{msg => "ldap_bind_failed", reason => Reason}
|
||||
LogMeta#{msg => "ldap_bind_failed", reason => emqx_utils:redact(Reason)}
|
||||
),
|
||||
{error, {unrecoverable_error, Reason}}
|
||||
end.
|
||||
|
@ -100,7 +100,9 @@ prepare_template(Config, State) ->
|
|||
do_prepare_template(maps:to_list(maps:with([bind_password], Config)), State).
|
||||
|
||||
do_prepare_template([{bind_password, V} | T], State) ->
|
||||
do_prepare_template(T, State#{bind_password_tokens => emqx_placeholder:preproc_tmpl(V)});
|
||||
%% This is sensitive data
|
||||
%% to reduce match cases, here we reuse the existing sensitive filter key: bind_password
|
||||
do_prepare_template(T, State#{bind_password => emqx_placeholder:preproc_tmpl(V)});
|
||||
do_prepare_template([], State) ->
|
||||
State.
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ post_boot() ->
|
|||
ok = ensure_apps_started(),
|
||||
ok = print_vsn(),
|
||||
ok = start_autocluster(),
|
||||
?AUDIT(alert, "from_cli", #{time => logger:timestamp(), event => "emqx_start"}),
|
||||
?AUDIT(alert, cli, #{time => logger:timestamp(), event => "emqx_start"}),
|
||||
ignore.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
|
|
@ -67,8 +67,9 @@ graceful() ->
|
|||
|
||||
%% @doc Shutdown the Erlang VM and wait indefinitely.
|
||||
graceful_wait() ->
|
||||
?AUDIT(alert, "from_cli", #{
|
||||
time => logger:timestamp(), msg => "run_emqx_stop_to_grace_shutdown"
|
||||
?AUDIT(alert, cli, #{
|
||||
time => logger:timestamp(),
|
||||
event => emqx_gracefully_stop
|
||||
}),
|
||||
ok = graceful(),
|
||||
exit_loop().
|
||||
|
|
|
@ -104,7 +104,7 @@ max_heap_size_warning(MF, Args) ->
|
|||
msg => "shell_process_exceed_max_heap_size",
|
||||
current_heap_size => HeapSize,
|
||||
function => MF,
|
||||
args => Args,
|
||||
args => pp_args(Args),
|
||||
max_heap_size => ?MAX_HEAP_SIZE
|
||||
})
|
||||
end.
|
||||
|
@ -112,24 +112,33 @@ max_heap_size_warning(MF, Args) ->
|
|||
log(_, {?MODULE, prompt_func}, [[{history, _}]]) ->
|
||||
ok;
|
||||
log(IsAllow, MF, Args) ->
|
||||
?AUDIT(warning, "from_remote_console", #{
|
||||
?AUDIT(warning, erlang_console, #{
|
||||
time => logger:timestamp(),
|
||||
function => MF,
|
||||
args => Args,
|
||||
args => pp_args(Args),
|
||||
permission => IsAllow
|
||||
}),
|
||||
to_console(IsAllow, MF, Args).
|
||||
|
||||
to_console(prohibited, MF, Args) ->
|
||||
warning("DANGEROUS FUNCTION: FORBIDDEN IN SHELL!!!!!", []),
|
||||
?SLOG(error, #{msg => "execute_function_in_shell_prohibited", function => MF, args => Args});
|
||||
?SLOG(error, #{
|
||||
msg => "execute_function_in_shell_prohibited",
|
||||
function => MF,
|
||||
args => pp_args(Args)
|
||||
});
|
||||
to_console(exempted, MF, Args) ->
|
||||
limit_warning(MF, Args),
|
||||
?SLOG(error, #{
|
||||
msg => "execute_dangerous_function_in_shell_exempted", function => MF, args => Args
|
||||
msg => "execute_dangerous_function_in_shell_exempted",
|
||||
function => MF,
|
||||
args => pp_args(Args)
|
||||
});
|
||||
to_console(ok, MF, Args) ->
|
||||
limit_warning(MF, Args).
|
||||
|
||||
warning(Format, Args) ->
|
||||
io:format(?RED_BG ++ Format ++ ?RESET ++ "~n", Args).
|
||||
|
||||
pp_args(Args) ->
|
||||
iolist_to_binary(io_lib:format("~0p", [Args])).
|
||||
|
|
|
@ -266,7 +266,7 @@ fields(node_status) ->
|
|||
})},
|
||||
{status, ?HOCON(?R_REF(status))}
|
||||
];
|
||||
fields({Type, with_name}) ->
|
||||
fields("with_name_" ++ Type) ->
|
||||
listener_struct_with_name(Type);
|
||||
fields(Type) ->
|
||||
listener_struct(Type).
|
||||
|
@ -308,7 +308,7 @@ listener_union_member_selector(Opts) ->
|
|||
|
||||
create_listener_schema(Opts) ->
|
||||
Schemas = [
|
||||
?R_REF(Mod, {Type, with_name})
|
||||
?R_REF(Mod, "with_name_" ++ Type)
|
||||
|| #{ref := ?R_REF(Mod, Type)} <- listeners_info(Opts)
|
||||
],
|
||||
Example = maps:remove(id, tcp_schema_example()),
|
||||
|
@ -399,7 +399,7 @@ list_listeners(get, #{query_string := Query}) ->
|
|||
end,
|
||||
{200, listener_status_by_id(NodeL)};
|
||||
list_listeners(post, #{body := Body}) ->
|
||||
create_listener(Body).
|
||||
create_listener(name, Body).
|
||||
|
||||
crud_listeners_by_id(get, #{bindings := #{id := Id}}) ->
|
||||
case find_listeners_by_id(Id) of
|
||||
|
@ -407,7 +407,7 @@ crud_listeners_by_id(get, #{bindings := #{id := Id}}) ->
|
|||
[L] -> {200, L}
|
||||
end;
|
||||
crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Body0}) ->
|
||||
case parse_listener_conf(Body0) of
|
||||
case parse_listener_conf(id, Body0) of
|
||||
{Id, Type, Name, Conf} ->
|
||||
case get_raw(Type, Name) of
|
||||
undefined ->
|
||||
|
@ -430,7 +430,7 @@ crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Body0}) ->
|
|||
{400, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_ID_INCONSISTENT}}
|
||||
end;
|
||||
crud_listeners_by_id(post, #{body := Body}) ->
|
||||
create_listener(Body);
|
||||
create_listener(id, Body);
|
||||
crud_listeners_by_id(delete, #{bindings := #{id := Id}}) ->
|
||||
{ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(Id),
|
||||
case find_listeners_by_id(Id) of
|
||||
|
@ -441,11 +441,10 @@ crud_listeners_by_id(delete, #{bindings := #{id := Id}}) ->
|
|||
{404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}}
|
||||
end.
|
||||
|
||||
parse_listener_conf(Conf0) ->
|
||||
parse_listener_conf(id, Conf0) ->
|
||||
Conf1 = maps:without([<<"running">>, <<"current_connections">>], Conf0),
|
||||
{TypeBin, Conf2} = maps:take(<<"type">>, Conf1),
|
||||
TypeAtom = binary_to_existing_atom(TypeBin),
|
||||
|
||||
case maps:take(<<"id">>, Conf2) of
|
||||
{IdBin, Conf3} ->
|
||||
{ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(IdBin),
|
||||
|
@ -454,13 +453,18 @@ parse_listener_conf(Conf0) ->
|
|||
false -> {error, listener_type_inconsistent}
|
||||
end;
|
||||
_ ->
|
||||
case maps:take(<<"name">>, Conf2) of
|
||||
{Name, Conf3} ->
|
||||
IdBin = <<TypeBin/binary, $:, Name/binary>>,
|
||||
{binary_to_atom(IdBin), TypeAtom, Name, Conf3};
|
||||
_ ->
|
||||
{error, listener_config_invalid}
|
||||
end
|
||||
{error, listener_config_invalid}
|
||||
end;
|
||||
parse_listener_conf(name, Conf0) ->
|
||||
Conf1 = maps:without([<<"running">>, <<"current_connections">>], Conf0),
|
||||
{TypeBin, Conf2} = maps:take(<<"type">>, Conf1),
|
||||
TypeAtom = binary_to_existing_atom(TypeBin),
|
||||
case maps:take(<<"name">>, Conf2) of
|
||||
{Name, Conf3} ->
|
||||
IdBin = <<TypeBin/binary, $:, Name/binary>>,
|
||||
{binary_to_atom(IdBin), TypeAtom, Name, Conf3};
|
||||
_ ->
|
||||
{error, listener_config_invalid}
|
||||
end.
|
||||
|
||||
stop_listeners_by_id(Method, Body = #{bindings := Bindings}) ->
|
||||
|
@ -832,8 +836,8 @@ tcp_schema_example() ->
|
|||
type => tcp
|
||||
}.
|
||||
|
||||
create_listener(Body) ->
|
||||
case parse_listener_conf(Body) of
|
||||
create_listener(From, Body) ->
|
||||
case parse_listener_conf(From, Body) of
|
||||
{Id, Type, Name, Conf} ->
|
||||
case create(Type, Name, Conf) of
|
||||
{ok, #{raw_config := _RawConf}} ->
|
||||
|
|
|
@ -156,6 +156,8 @@ authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) ->
|
|||
{error, <<"not_allowed">>};
|
||||
authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) ->
|
||||
{error, <<"not_allowed">>};
|
||||
authorize(<<"/api/v5/logout", _/binary>>, _ApiKey, _ApiSecret) ->
|
||||
{error, <<"not_allowed">>};
|
||||
authorize(_Path, ApiKey, ApiSecret) ->
|
||||
Now = erlang:system_time(second),
|
||||
case find_by_api_key(ApiKey) of
|
||||
|
|
|
@ -482,7 +482,7 @@ trace(_) ->
|
|||
{"trace stop topic <Topic> ", "Stop tracing for a topic on local node"},
|
||||
{"trace start ip_address <IP> <File> [<Level>] ",
|
||||
"Traces for a client ip on local node"},
|
||||
{"trace stop ip_addresss <IP> ", "Stop tracing for a client ip on local node"}
|
||||
{"trace stop ip_address <IP> ", "Stop tracing for a client ip on local node"}
|
||||
]).
|
||||
|
||||
trace_on(Name, Type, Filter, Level, LogFile) ->
|
||||
|
|
|
@ -238,7 +238,6 @@ t_clear_certs(Config) when is_list(Config) ->
|
|||
NewConf2 = emqx_utils_maps:deep_put(
|
||||
[<<"ssl_options">>, <<"keyfile">>], NewConf, cert_file("keyfile")
|
||||
),
|
||||
|
||||
_ = request(post, NewPath, [], NewConf2),
|
||||
ListResult1 = list_pem_dir("ssl", "clear"),
|
||||
?assertMatch({ok, [_, _]}, ListResult1),
|
||||
|
@ -251,7 +250,7 @@ t_clear_certs(Config) when is_list(Config) ->
|
|||
_ = emqx_tls_certfile_gc:force(),
|
||||
ListResult2 = list_pem_dir("ssl", "clear"),
|
||||
|
||||
%% make sure the old cret file is deleted
|
||||
%% make sure the old cert file is deleted
|
||||
?assertMatch({ok, [_, _]}, ListResult2),
|
||||
|
||||
{ok, ResultList1} = ListResult1,
|
||||
|
@ -273,6 +272,17 @@ t_clear_certs(Config) when is_list(Config) ->
|
|||
_ = delete(NewPath),
|
||||
_ = emqx_tls_certfile_gc:force(),
|
||||
?assertMatch({error, enoent}, list_pem_dir("ssl", "clear")),
|
||||
|
||||
%% test create listeners without id in path
|
||||
NewPath1 = emqx_mgmt_api_test_util:api_path(["listeners"]),
|
||||
NewConf3 = maps:remove(<<"id">>, NewConf2#{<<"name">> => <<"clear">>}),
|
||||
?assertNotMatch({error, {"HTTP/1.1", 400, _}}, request(post, NewPath1, [], NewConf3)),
|
||||
ListResult3 = list_pem_dir("ssl", "clear"),
|
||||
?assertMatch({ok, [_, _]}, ListResult3),
|
||||
_ = delete(NewPath),
|
||||
_ = emqx_tls_certfile_gc:force(),
|
||||
?assertMatch({error, enoent}, list_pem_dir("ssl", "clear")),
|
||||
|
||||
ok.
|
||||
|
||||
get_tcp_listeners(Node) ->
|
||||
|
|
|
@ -223,9 +223,10 @@ restart(ResId, Opts) when is_binary(ResId) ->
|
|||
%% @doc Start the resource
|
||||
-spec start(resource_id(), creation_opts()) -> ok | {error, Reason :: term()}.
|
||||
start(ResId, Opts) ->
|
||||
case safe_call(ResId, start, ?T_OPERATION) of
|
||||
StartTimeout = maps:get(start_timeout, Opts, ?T_OPERATION),
|
||||
case safe_call(ResId, start, StartTimeout) of
|
||||
ok ->
|
||||
wait_for_ready(ResId, maps:get(start_timeout, Opts, 5000));
|
||||
wait_for_ready(ResId, StartTimeout);
|
||||
{error, _Reason} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
|
|
@ -278,7 +278,7 @@ t_crud(Config) ->
|
|||
<<"code">> := <<"BAD_REQUEST">>,
|
||||
<<"message">> :=
|
||||
#{
|
||||
<<"expected">> := [_ | _],
|
||||
<<"expected">> := <<"avro | protobuf">>,
|
||||
<<"field_name">> := <<"type">>
|
||||
}
|
||||
}},
|
||||
|
@ -301,7 +301,7 @@ t_crud(Config) ->
|
|||
<<"code">> := <<"BAD_REQUEST">>,
|
||||
<<"message">> :=
|
||||
#{
|
||||
<<"expected">> := [_ | _],
|
||||
<<"expected">> := <<"avro | protobuf">>,
|
||||
<<"field_name">> := <<"type">>
|
||||
}
|
||||
}},
|
||||
|
|
|
@ -61,7 +61,8 @@
|
|||
diff_lists/3,
|
||||
merge_lists/3,
|
||||
tcp_keepalive_opts/4,
|
||||
format/1
|
||||
format/1,
|
||||
format_mfal/1
|
||||
]).
|
||||
|
||||
-export([
|
||||
|
@ -529,6 +530,30 @@ tcp_keepalive_opts(OS, _Idle, _Interval, _Probes) ->
|
|||
format(Term) ->
|
||||
iolist_to_binary(io_lib:format("~0p", [Term])).
|
||||
|
||||
%% @doc Helper function for log formatters.
|
||||
-spec format_mfal(map()) -> undefined | binary().
|
||||
format_mfal(Data) ->
|
||||
Line =
|
||||
case maps:get(line, Data, undefined) of
|
||||
undefined ->
|
||||
<<"">>;
|
||||
Num ->
|
||||
["(", integer_to_list(Num), ")"]
|
||||
end,
|
||||
case maps:get(mfa, Data, undefined) of
|
||||
{M, F, A} ->
|
||||
iolist_to_binary([
|
||||
atom_to_binary(M, utf8),
|
||||
$:,
|
||||
atom_to_binary(F, utf8),
|
||||
$/,
|
||||
integer_to_binary(A),
|
||||
Line
|
||||
]);
|
||||
_ ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal Functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -620,6 +645,7 @@ try_to_existing_atom(Convert, Data, Encoding) ->
|
|||
_:Reason -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% NOTE: keep alphabetical order
|
||||
is_sensitive_key(aws_secret_access_key) -> true;
|
||||
is_sensitive_key("aws_secret_access_key") -> true;
|
||||
is_sensitive_key(<<"aws_secret_access_key">>) -> true;
|
||||
|
@ -641,6 +667,8 @@ is_sensitive_key(<<"secret_key">>) -> true;
|
|||
is_sensitive_key(security_token) -> true;
|
||||
is_sensitive_key("security_token") -> true;
|
||||
is_sensitive_key(<<"security_token">>) -> true;
|
||||
is_sensitive_key(sp_private_key) -> true;
|
||||
is_sensitive_key(<<"sp_private_key">>) -> true;
|
||||
is_sensitive_key(token) -> true;
|
||||
is_sensitive_key("token") -> true;
|
||||
is_sensitive_key(<<"token">>) -> true;
|
||||
|
|
|
@ -140,11 +140,12 @@ do(Args) ->
|
|||
io:format("~p\n", [Other])
|
||||
end;
|
||||
["eval" | ListOfArgs] ->
|
||||
% parse args locally in the remsh node
|
||||
Parsed = parse_eval_args(ListOfArgs),
|
||||
% and evaluate it on the remote node
|
||||
case rpc:call(TargetNode, emqx_ctl, eval_erl, [Parsed]) of
|
||||
case rpc:call(TargetNode, emqx_ctl, run_command, [eval_erl, Parsed], infinity) of
|
||||
{ok, Value} ->
|
||||
io:format("~p~n",[Value]);
|
||||
io:format("~p~n", [Value]);
|
||||
{badrpc, Reason} ->
|
||||
io:format("RPC to ~p failed: ~p~n", [TargetNode, Reason]),
|
||||
halt(1)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Fixed an issue where logging would stop if "Rotation Size" would be set to `infinity` on file log handlers.
|
|
@ -0,0 +1,3 @@
|
|||
Fix log formatter when log.HANDLER.formatter is set to 'json'.
|
||||
|
||||
The bug was introduced in v5.0.4 where the log line was no longer a valid JSON, but prefixed with timestamp string and level name.
|
|
@ -0,0 +1 @@
|
|||
Disable access to the `logout` endpoint by the API key, this endpoint is for the Dashboard only.
|
|
@ -0,0 +1 @@
|
|||
Make dashboard login support SSO (Single Sign On) using LDAP.
|
|
@ -0,0 +1 @@
|
|||
Make dashboard login support SSO (Single Sign On) using SAML 2.0.
|
|
@ -14,8 +14,8 @@ type: application
|
|||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
version: 5.3.0-alpha.1
|
||||
version: 5.3.0-alpha.2
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application.
|
||||
appVersion: 5.3.0-alpha.1
|
||||
appVersion: 5.3.0-alpha.2
|
||||
|
|
2
dev
2
dev
|
@ -416,7 +416,7 @@ boot() {
|
|||
gen_tmp_node_name() {
|
||||
local rnd
|
||||
rnd="$(od -t u -N 4 /dev/urandom | head -n1 | awk '{print $2 % 1000}')"
|
||||
echo "remsh${rnd}-$EMQX_NODE_NAME}"
|
||||
echo "remsh${rnd}-${EMQX_NODE_NAME}"
|
||||
}
|
||||
|
||||
remsh() {
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -835,7 +835,7 @@ defmodule EMQXUmbrella.MixProject do
|
|||
defp quicer_dep() do
|
||||
if enable_quicer?(),
|
||||
# in conflict with emqx and emqtt
|
||||
do: [{:quicer, github: "emqx/quic", tag: "0.0.200", override: true}],
|
||||
do: [{:quicer, github: "emqx/quic", tag: "0.0.201", override: true}],
|
||||
else: []
|
||||
end
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ bcrypt() ->
|
|||
{bcrypt, {git, "https://github.com/emqx/erlang-bcrypt.git", {tag, "0.6.1"}}}.
|
||||
|
||||
quicer() ->
|
||||
{quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.200"}}}.
|
||||
{quicer, {git, "https://github.com/emqx/quic.git", {tag, "0.0.201"}}}.
|
||||
|
||||
jq() ->
|
||||
{jq, {git, "https://github.com/emqx/jq", {tag, "v0.3.10"}}}.
|
||||
|
|
|
@ -101,7 +101,7 @@ common_handler_flush_qlen.label:
|
|||
|
||||
common_handler_chars_limit.desc:
|
||||
"""Set the maximum length of a single log message. If this length is exceeded, the log message will be truncated.
|
||||
NOTE: Restrict char limiter if formatter is JSON , it will get a truncated incomplete JSON data, which is not recommended."""
|
||||
When formatter is <code>json</code> the truncation is done on the JSON values, but not on the log message itself."""
|
||||
|
||||
common_handler_chars_limit.label:
|
||||
"""Single Log Max Length"""
|
||||
|
@ -660,7 +660,8 @@ Can be one of:
|
|||
- <code>system</code>: the time offset used by the local system
|
||||
- <code>utc</code>: the UTC time offset
|
||||
- <code>+-[hh]:[mm]</code>: user specified time offset, such as "-02:00" or "+00:00"
|
||||
Defaults to: <code>system</code>."""
|
||||
Defaults to: <code>system</code>.
|
||||
This config has no effect for when formatter is <code>json</code> as the timestamp in JSON is milliseconds since epoch."""
|
||||
|
||||
common_handler_time_offset.label:
|
||||
"""Time Offset"""
|
||||
|
@ -841,7 +842,4 @@ Defaults to 100000."""
|
|||
node_channel_cleanup_batch_size.label:
|
||||
"""Node Channel Cleanup Batch Size"""
|
||||
|
||||
prevent_overlapping_partitions.desc:
|
||||
"""https://www.erlang.org/doc/man/global.html#description"""
|
||||
|
||||
}
|
||||
|
|
|
@ -43,7 +43,9 @@ login_success.desc:
|
|||
"""Dashboard Auth Success"""
|
||||
|
||||
logout_api.desc:
|
||||
"""Dashboard user logout"""
|
||||
"""Dashboard user logout.
|
||||
This endpoint is only for the Dashboard, not the `API Key`.
|
||||
The token from the `/login` endpoint must be a bearer authorization in the headers."""
|
||||
logout_api.label:
|
||||
"""Dashboard user logout"""
|
||||
|
||||
|
|
|
@ -51,4 +51,16 @@ backend_name.desc:
|
|||
backend_name.label:
|
||||
"""Backend Name"""
|
||||
|
||||
running.desc:
|
||||
"""Is the backend running"""
|
||||
|
||||
running.label:
|
||||
"""Running"""
|
||||
|
||||
last_error.desc:
|
||||
"""Last error of this backend"""
|
||||
|
||||
last_error.label:
|
||||
"""Last Error"""
|
||||
|
||||
}
|
||||
|
|
|
@ -166,6 +166,7 @@ ip
|
|||
ipv
|
||||
jenkins
|
||||
jq
|
||||
json
|
||||
kb
|
||||
keepalive
|
||||
keyfile
|
||||
|
|
|
@ -12,10 +12,18 @@
|
|||
[[ "$output" =~ "ERROR: Invalid node name,".+ ]]
|
||||
}
|
||||
|
||||
@test "corrupted cluster config file" {
|
||||
@test "corrupted cluster-override.conf" {
|
||||
conffile="./_build/$PROFILE/rel/emqx/data/configs/cluster-override.conf"
|
||||
echo "{" > $conffile
|
||||
run ./_build/$PROFILE/rel/emqx/bin/emqx console
|
||||
[[ $status -ne 0 ]]
|
||||
rm -f $conffile
|
||||
}
|
||||
|
||||
@test "corrupted cluster.hocon" {
|
||||
conffile="./_build/$PROFILE/rel/emqx/data/configs/cluster.hocon"
|
||||
echo "{" > $conffile
|
||||
run ./_build/$PROFILE/rel/emqx/bin/emqx console
|
||||
[[ $status -ne 0 ]]
|
||||
rm -f $conffile
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue