fix(audit): make emqx eval command auditable

This commit is contained in:
Zaiming (Stone) Shi 2023-09-22 11:22:15 +02:00
parent 2e9f451df3
commit a34ab19d93
4 changed files with 82 additions and 18 deletions

View File

@ -44,6 +44,10 @@
usage/2 usage/2
]). ]).
-export([
eval_erl/1
]).
%% Exports mainly for test cases %% Exports mainly for test cases
-export([ -export([
format/2, format/2,
@ -119,7 +123,7 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
Start = erlang:monotonic_time(), Start = erlang:monotonic_time(),
Result = Result =
case lookup_command(Cmd) of case lookup_command(Cmd) of
[{Mod, Fun}] -> {ok, {Mod, Fun}} ->
try try
apply(Mod, Fun, [Args]) apply(Mod, Fun, [Args])
catch catch
@ -127,13 +131,15 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
?LOG_ERROR(#{ ?LOG_ERROR(#{
msg => "ctl_command_crashed", msg => "ctl_command_crashed",
stacktrace => Stacktrace, stacktrace => Stacktrace,
reason => Reason reason => Reason,
module => Mod,
function => Fun
}), }),
{error, Reason} {error, Reason}
end; end;
Error -> {error, Reason} ->
help(), help(),
Error {error, Reason}
end, end,
Duration = erlang:convert_time_unit(erlang:monotonic_time() - Start, native, millisecond), Duration = erlang:convert_time_unit(erlang:monotonic_time() - Start, native, millisecond),
@ -144,12 +150,22 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
), ),
Result. Result.
-spec lookup_command(cmd()) -> [{module(), atom()}] | {error, any()}. -spec lookup_command(cmd()) -> {module(), atom()} | {error, any()}.
lookup_command(eval_erl) ->
%% So far 'emqx ctl eval_erl Expr' is a undocumented hidden command.
%% For backward compatibility,
%% the documented command 'emqx eval Expr' has the expression parsed
%% in the remsh node (nodetool).
%%
%% 'eval_erl' is added for two purposes
%% 1. 'emqx eval Expr' can be audited
%% 2. 'emqx ctl eval_erl Expr' simplifies the scripting part
{ok, {?MODULE, eval_erl}};
lookup_command(Cmd) when is_atom(Cmd) -> lookup_command(Cmd) when is_atom(Cmd) ->
case is_initialized() of case is_initialized() of
true -> true ->
case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of
[El] -> El; [[{M, F}]] -> {ok, {M, F}};
[] -> {error, cmd_not_found} [] -> {error, cmd_not_found}
end; end;
false -> false ->
@ -319,7 +335,7 @@ audit_log(Level, From, Log) ->
case lookup_command(audit) of case lookup_command(audit) of
{error, _} -> {error, _} ->
ignore; ignore;
[{Mod, Fun}] -> {ok, {Mod, Fun}} ->
try try
apply(Mod, Fun, [Level, From, Log]) apply(Mod, Fun, [Level, From, Log])
catch catch
@ -339,3 +355,23 @@ audit_level({ok, _}, Duration) when Duration >= ?TOO_SLOW -> warning;
audit_level(ok, _Duration) -> info; audit_level(ok, _Duration) -> info;
audit_level({ok, _}, _Duration) -> info; audit_level({ok, _}, _Duration) -> info;
audit_level(_, _) -> error. audit_level(_, _) -> error.
eval_erl([Parsed | _] = Expr) when is_tuple(Parsed) ->
eval_expr(Expr);
eval_erl([String]) ->
% convenience to users, if they forgot a trailing
% '.' add it for them.
Normalized =
case lists:reverse(String) of
[$. | _] -> String;
R -> lists:reverse([$. | R])
end,
% then scan and parse the string
{ok, Scanned, _} = erl_scan:string(Normalized),
{ok, Parsed} = erl_parse:parse_exprs(Scanned),
{ok, Value} = eval_expr(Parsed),
print("~p~n", [Value]).
eval_expr(Parsed) ->
{value, Value, _} = erl_eval:exprs(Parsed, []),
{ok, Value}.

View File

@ -40,8 +40,8 @@ t_reg_unreg_command(_) ->
fun(_CtlSrv) -> fun(_CtlSrv) ->
emqx_ctl:register_command(cmd1, {?MODULE, cmd1_fun}), emqx_ctl:register_command(cmd1, {?MODULE, cmd1_fun}),
emqx_ctl:register_command(cmd2, {?MODULE, cmd2_fun}), emqx_ctl:register_command(cmd2, {?MODULE, cmd2_fun}),
?assertEqual([{?MODULE, cmd1_fun}], emqx_ctl:lookup_command(cmd1)), ?assertEqual({?MODULE, cmd1_fun}, lookup_command(cmd1)),
?assertEqual([{?MODULE, cmd2_fun}], emqx_ctl:lookup_command(cmd2)), ?assertEqual({?MODULE, cmd2_fun}, lookup_command(cmd2)),
?assertEqual( ?assertEqual(
[{cmd1, ?MODULE, cmd1_fun}, {cmd2, ?MODULE, cmd2_fun}], [{cmd1, ?MODULE, cmd1_fun}, {cmd2, ?MODULE, cmd2_fun}],
emqx_ctl:get_commands() emqx_ctl:get_commands()
@ -49,8 +49,8 @@ t_reg_unreg_command(_) ->
emqx_ctl:unregister_command(cmd1), emqx_ctl:unregister_command(cmd1),
emqx_ctl:unregister_command(cmd2), emqx_ctl:unregister_command(cmd2),
ct:sleep(100), ct:sleep(100),
?assertEqual({error, cmd_not_found}, emqx_ctl:lookup_command(cmd1)), ?assertEqual({error, cmd_not_found}, lookup_command(cmd1)),
?assertEqual({error, cmd_not_found}, emqx_ctl:lookup_command(cmd2)), ?assertEqual({error, cmd_not_found}, lookup_command(cmd2)),
?assertEqual([], emqx_ctl:get_commands()) ?assertEqual([], emqx_ctl:get_commands())
end end
). ).
@ -79,6 +79,12 @@ t_print(_) ->
?assertEqual("~!@#$%^&*()", emqx_ctl:print("~ts", [<<"~!@#$%^&*()">>])), ?assertEqual("~!@#$%^&*()", emqx_ctl:print("~ts", [<<"~!@#$%^&*()">>])),
unmock_print(). unmock_print().
t_eval_erl(_) ->
mock_print(),
Expected = atom_to_list(node()) ++ "\n",
?assertEqual(Expected, emqx_ctl:run_command(["eval_erl", "node()"])),
unmock_print().
t_usage(_) -> t_usage(_) ->
CmdParams1 = "emqx_cmd_1 param1 param2", CmdParams1 = "emqx_cmd_1 param1 param2",
CmdDescr1 = "emqx_cmd_1 is a test command means nothing", CmdDescr1 = "emqx_cmd_1 is a test command means nothing",
@ -129,3 +135,9 @@ mock_print() ->
unmock_print() -> unmock_print() ->
meck:unload(emqx_ctl). meck:unload(emqx_ctl).
lookup_command(Cmd) ->
case emqx_ctl:lookup_command(Cmd) of
{ok, {Mod, Fun}} -> {Mod, Fun};
Error -> Error
end.

View File

@ -142,8 +142,8 @@ do(Args) ->
["eval" | ListOfArgs] -> ["eval" | ListOfArgs] ->
Parsed = parse_eval_args(ListOfArgs), Parsed = parse_eval_args(ListOfArgs),
% and evaluate it on the remote node % and evaluate it on the remote node
case rpc:call(TargetNode, erl_eval, exprs, [Parsed, [] ]) of case rpc:call(TargetNode, emqx_ctl, eval_erl, [Parsed]) of
{value, Value, _} -> {ok, Value} ->
io:format("~p~n",[Value]); io:format("~p~n",[Value]);
{badrpc, Reason} -> {badrpc, Reason} ->
io:format("RPC to ~p failed: ~p~n", [TargetNode, Reason]), io:format("RPC to ~p failed: ~p~n", [TargetNode, Reason]),

24
dev
View File

@ -37,6 +37,7 @@ COMMANDS:
ctl: Equivalent to 'emqx ctl'. ctl: Equivalent to 'emqx ctl'.
ctl command arguments should be passed after '--' ctl command arguments should be passed after '--'
e.g. $0 ctl -- help e.g. $0 ctl -- help
eval: Evaluate an Erlang expression
OPTIONS: OPTIONS:
-p|--profile: emqx | emqx-enterprise, defaults to 'PROFILE' env. -p|--profile: emqx | emqx-enterprise, defaults to 'PROFILE' env.
@ -83,6 +84,10 @@ case "${1:-novalue}" in
COMMAND='ctl' COMMAND='ctl'
shift shift
;; ;;
eval)
COMMAND='eval'
shift
;;
help) help)
usage usage
exit 0 exit 0
@ -425,14 +430,22 @@ remsh() {
$EPMD_ARGS $EPMD_ARGS
} }
# evaluate erlang expression in remsh node
eval_remsh_erl() {
local tmpnode erl_code
tmpnode="$(gen_tmp_node_name)"
erl_code="$1"
# shellcheck disable=SC2086 # need to expand EMQD_ARGS
erl -name "$tmpnode" -setcookie "$COOKIE" -hidden -noshell $EPMD_ARGS -eval "$erl_code" 2>&1
}
ctl() { ctl() {
if [ -z "${PASSTHROUGH_ARGS:-}" ]; then if [ -z "${PASSTHROUGH_ARGS:-}" ]; then
logerr "Need at least one argument for ctl command" logerr "Need at least one argument for ctl command"
logerr "e.g. $0 ctl -- help" logerr "e.g. $0 ctl -- help"
exit 1 exit 1
fi fi
local tmpnode args rpc_code output result local args rpc_code output result
tmpnode="$(gen_tmp_node_name)"
args="$(make_erlang_args "${PASSTHROUGH_ARGS[@]}")" args="$(make_erlang_args "${PASSTHROUGH_ARGS[@]}")"
rpc_code=" rpc_code="
case rpc:call('$EMQX_NODE_NAME', emqx_ctl, run_command, [[$args]]) of case rpc:call('$EMQX_NODE_NAME', emqx_ctl, run_command, [[$args]]) of
@ -443,8 +456,7 @@ ctl() {
init:stop(1) init:stop(1)
end" end"
set +e set +e
# shellcheck disable=SC2086 output="$(eval_remsh_erl "$rpc_code")"
output="$(erl -name "$tmpnode" -setcookie "$COOKIE" -hidden -noshell $EPMD_ARGS -eval "$rpc_code" 2>&1)"
result=$? result=$?
if [ $result -eq 0 ]; then if [ $result -eq 0 ]; then
echo -e "$output" echo -e "$output"
@ -464,4 +476,8 @@ case "$COMMAND" in
ctl) ctl)
ctl ctl
;; ;;
eval)
PASSTHROUGH_ARGS=('eval_erl' "${PASSTHROUGH_ARGS[@]}")
ctl
;;
esac esac