Merge pull request #9593 from lafirest/fix/mysql_response
fix(bridges): obfuscate the password in bridges API responses
This commit is contained in:
commit
3dd4dbd887
|
@ -68,7 +68,7 @@
|
||||||
nolink_apply/2
|
nolink_apply/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([clamp/3]).
|
-export([clamp/3, redact/1, redact/2, is_redacted/2, is_redacted/3]).
|
||||||
|
|
||||||
-dialyzer({nowarn_function, [nolink_apply/2]}).
|
-dialyzer({nowarn_function, [nolink_apply/2]}).
|
||||||
|
|
||||||
|
@ -556,6 +556,75 @@ try_to_existing_atom(Convert, Data, Encoding) ->
|
||||||
_:Reason -> {error, Reason}
|
_:Reason -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
is_sensitive_key(token) -> true;
|
||||||
|
is_sensitive_key("token") -> true;
|
||||||
|
is_sensitive_key(<<"token">>) -> true;
|
||||||
|
is_sensitive_key(password) -> true;
|
||||||
|
is_sensitive_key("password") -> true;
|
||||||
|
is_sensitive_key(<<"password">>) -> true;
|
||||||
|
is_sensitive_key(secret) -> true;
|
||||||
|
is_sensitive_key("secret") -> true;
|
||||||
|
is_sensitive_key(<<"secret">>) -> true;
|
||||||
|
is_sensitive_key(_) -> false.
|
||||||
|
|
||||||
|
redact(Term) ->
|
||||||
|
do_redact(Term, fun is_sensitive_key/1).
|
||||||
|
|
||||||
|
redact(Term, Checker) ->
|
||||||
|
do_redact(Term, fun(V) ->
|
||||||
|
is_sensitive_key(V) orelse Checker(V)
|
||||||
|
end).
|
||||||
|
|
||||||
|
do_redact(L, Checker) when is_list(L) ->
|
||||||
|
lists:map(fun(E) -> do_redact(E, Checker) end, L);
|
||||||
|
do_redact(M, Checker) when is_map(M) ->
|
||||||
|
maps:map(
|
||||||
|
fun(K, V) ->
|
||||||
|
do_redact(K, V, Checker)
|
||||||
|
end,
|
||||||
|
M
|
||||||
|
);
|
||||||
|
do_redact({Key, Value}, Checker) ->
|
||||||
|
case Checker(Key) of
|
||||||
|
true ->
|
||||||
|
{Key, redact_v(Value)};
|
||||||
|
false ->
|
||||||
|
{do_redact(Key, Checker), do_redact(Value, Checker)}
|
||||||
|
end;
|
||||||
|
do_redact(T, Checker) when is_tuple(T) ->
|
||||||
|
Elements = erlang:tuple_to_list(T),
|
||||||
|
Redact = do_redact(Elements, Checker),
|
||||||
|
erlang:list_to_tuple(Redact);
|
||||||
|
do_redact(Any, _Checker) ->
|
||||||
|
Any.
|
||||||
|
|
||||||
|
do_redact(K, V, Checker) ->
|
||||||
|
case Checker(K) of
|
||||||
|
true ->
|
||||||
|
redact_v(V);
|
||||||
|
false ->
|
||||||
|
do_redact(V, Checker)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-define(REDACT_VAL, "******").
|
||||||
|
redact_v(V) when is_binary(V) -> <<?REDACT_VAL>>;
|
||||||
|
redact_v(_V) -> ?REDACT_VAL.
|
||||||
|
|
||||||
|
is_redacted(K, V) ->
|
||||||
|
do_is_redacted(K, V, fun is_sensitive_key/1).
|
||||||
|
|
||||||
|
is_redacted(K, V, Fun) ->
|
||||||
|
do_is_redacted(K, V, fun(E) ->
|
||||||
|
is_sensitive_key(E) orelse Fun(E)
|
||||||
|
end).
|
||||||
|
|
||||||
|
do_is_redacted(K, ?REDACT_VAL, Fun) ->
|
||||||
|
Fun(K);
|
||||||
|
do_is_redacted(K, <<?REDACT_VAL>>, Fun) ->
|
||||||
|
Fun(K);
|
||||||
|
do_is_redacted(_K, _V, _Fun) ->
|
||||||
|
false.
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
@ -568,6 +637,62 @@ ipv6_probe_test() ->
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
redact_test_() ->
|
||||||
|
Case = fun(Type, KeyT) ->
|
||||||
|
Key =
|
||||||
|
case Type of
|
||||||
|
atom -> KeyT;
|
||||||
|
string -> erlang:atom_to_list(KeyT);
|
||||||
|
binary -> erlang:atom_to_binary(KeyT)
|
||||||
|
end,
|
||||||
|
|
||||||
|
?assert(is_sensitive_key(Key)),
|
||||||
|
|
||||||
|
%% direct
|
||||||
|
?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo})),
|
||||||
|
?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo})),
|
||||||
|
?assertEqual({Key, Key, Key}, redact({Key, Key, Key})),
|
||||||
|
?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar})),
|
||||||
|
|
||||||
|
%% 1 level nested
|
||||||
|
?assertEqual([{Key, ?REDACT_VAL}], redact([{Key, foo}])),
|
||||||
|
?assertEqual([#{Key => ?REDACT_VAL}], redact([#{Key => foo}])),
|
||||||
|
|
||||||
|
%% 2 level nested
|
||||||
|
?assertEqual(#{opts => [{Key, ?REDACT_VAL}]}, redact(#{opts => [{Key, foo}]})),
|
||||||
|
?assertEqual(#{opts => #{Key => ?REDACT_VAL}}, redact(#{opts => #{Key => foo}})),
|
||||||
|
?assertEqual({opts, [{Key, ?REDACT_VAL}]}, redact({opts, [{Key, foo}]})),
|
||||||
|
|
||||||
|
%% 3 level nested
|
||||||
|
?assertEqual([#{opts => [{Key, ?REDACT_VAL}]}], redact([#{opts => [{Key, foo}]}])),
|
||||||
|
?assertEqual([{opts, [{Key, ?REDACT_VAL}]}], redact([{opts, [{Key, foo}]}])),
|
||||||
|
?assertEqual([{opts, [#{Key => ?REDACT_VAL}]}], redact([{opts, [#{Key => foo}]}]))
|
||||||
|
end,
|
||||||
|
|
||||||
|
Types = [atom, string, binary],
|
||||||
|
Keys = [
|
||||||
|
token,
|
||||||
|
password,
|
||||||
|
secret
|
||||||
|
],
|
||||||
|
[{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types].
|
||||||
|
|
||||||
|
redact2_test_() ->
|
||||||
|
Case = fun(Key, Checker) ->
|
||||||
|
?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo}, Checker)),
|
||||||
|
?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo}, Checker)),
|
||||||
|
?assertEqual({Key, Key, Key}, redact({Key, Key, Key}, Checker)),
|
||||||
|
?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar}, Checker))
|
||||||
|
end,
|
||||||
|
|
||||||
|
Checker = fun(E) -> E =:= passcode end,
|
||||||
|
|
||||||
|
Keys = [secret, passcode],
|
||||||
|
[{case_name(atom, Key), fun() -> Case(Key, Checker) end} || Key <- Keys].
|
||||||
|
|
||||||
|
case_name(Type, Key) ->
|
||||||
|
lists:concat([Type, "-", Key]).
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
pub_props_to_packet(Properties) ->
|
pub_props_to_packet(Properties) ->
|
||||||
|
|
|
@ -409,11 +409,13 @@ schema("/nodes/:node/bridges/:id/operation/:operation") ->
|
||||||
'/bridges/:id'(get, #{bindings := #{id := Id}}) ->
|
'/bridges/:id'(get, #{bindings := #{id := Id}}) ->
|
||||||
?TRY_PARSE_ID(Id, lookup_from_all_nodes(BridgeType, BridgeName, 200));
|
?TRY_PARSE_ID(Id, lookup_from_all_nodes(BridgeType, BridgeName, 200));
|
||||||
'/bridges/:id'(put, #{bindings := #{id := Id}, body := Conf0}) ->
|
'/bridges/:id'(put, #{bindings := #{id := Id}, body := Conf0}) ->
|
||||||
Conf = filter_out_request_body(Conf0),
|
Conf1 = filter_out_request_body(Conf0),
|
||||||
?TRY_PARSE_ID(
|
?TRY_PARSE_ID(
|
||||||
Id,
|
Id,
|
||||||
case emqx_bridge:lookup(BridgeType, BridgeName) of
|
case emqx_bridge:lookup(BridgeType, BridgeName) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
|
RawConf = emqx:get_raw_config([bridges, BridgeType, BridgeName], #{}),
|
||||||
|
Conf = deobfuscate(Conf1, RawConf),
|
||||||
case ensure_bridge_created(BridgeType, BridgeName, Conf) of
|
case ensure_bridge_created(BridgeType, BridgeName, Conf) of
|
||||||
ok ->
|
ok ->
|
||||||
lookup_from_all_nodes(BridgeType, BridgeName, 200);
|
lookup_from_all_nodes(BridgeType, BridgeName, 200);
|
||||||
|
@ -604,12 +606,12 @@ format_bridge_info([FirstBridge | _] = Bridges) ->
|
||||||
Res = maps:remove(node, FirstBridge),
|
Res = maps:remove(node, FirstBridge),
|
||||||
NodeStatus = collect_status(Bridges),
|
NodeStatus = collect_status(Bridges),
|
||||||
NodeMetrics = collect_metrics(Bridges),
|
NodeMetrics = collect_metrics(Bridges),
|
||||||
Res#{
|
redact(Res#{
|
||||||
status => aggregate_status(NodeStatus),
|
status => aggregate_status(NodeStatus),
|
||||||
node_status => NodeStatus,
|
node_status => NodeStatus,
|
||||||
metrics => aggregate_metrics(NodeMetrics),
|
metrics => aggregate_metrics(NodeMetrics),
|
||||||
node_metrics => NodeMetrics
|
node_metrics => NodeMetrics
|
||||||
}.
|
}).
|
||||||
|
|
||||||
collect_status(Bridges) ->
|
collect_status(Bridges) ->
|
||||||
[maps:with([node, status], B) || B <- Bridges].
|
[maps:with([node, status], B) || B <- Bridges].
|
||||||
|
@ -676,13 +678,13 @@ format_resp(
|
||||||
Node
|
Node
|
||||||
) ->
|
) ->
|
||||||
RawConfFull = fill_defaults(Type, RawConf),
|
RawConfFull = fill_defaults(Type, RawConf),
|
||||||
RawConfFull#{
|
redact(RawConfFull#{
|
||||||
type => Type,
|
type => Type,
|
||||||
name => maps:get(<<"name">>, RawConf, BridgeName),
|
name => maps:get(<<"name">>, RawConf, BridgeName),
|
||||||
node => Node,
|
node => Node,
|
||||||
status => Status,
|
status => Status,
|
||||||
metrics => format_metrics(Metrics)
|
metrics => format_metrics(Metrics)
|
||||||
}.
|
}).
|
||||||
|
|
||||||
format_metrics(#{
|
format_metrics(#{
|
||||||
counters := #{
|
counters := #{
|
||||||
|
@ -806,3 +808,27 @@ call_operation(Node, OperFunc, BridgeType, BridgeName) ->
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
{400, error_msg('INVALID_NODE', <<"invalid node">>)}
|
{400, error_msg('INVALID_NODE', <<"invalid node">>)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
redact(Term) ->
|
||||||
|
emqx_misc:redact(Term).
|
||||||
|
|
||||||
|
deobfuscate(NewConf, OldConf) ->
|
||||||
|
maps:fold(
|
||||||
|
fun(K, V, Acc) ->
|
||||||
|
case maps:find(K, OldConf) of
|
||||||
|
error ->
|
||||||
|
Acc#{K => V};
|
||||||
|
{ok, OldV} when is_map(V), is_map(OldV) ->
|
||||||
|
Acc#{K => deobfuscate(V, OldV)};
|
||||||
|
{ok, OldV} ->
|
||||||
|
case emqx_misc:is_redacted(K, V) of
|
||||||
|
true ->
|
||||||
|
Acc#{K => OldV};
|
||||||
|
_ ->
|
||||||
|
Acc#{K => V}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
NewConf
|
||||||
|
).
|
||||||
|
|
|
@ -514,6 +514,39 @@ t_reset_bridges(Config) ->
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
|
||||||
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
|
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
|
||||||
|
|
||||||
|
t_with_redact_update(_Config) ->
|
||||||
|
Name = <<"redact_update">>,
|
||||||
|
Type = <<"mqtt">>,
|
||||||
|
Password = <<"123456">>,
|
||||||
|
Template = #{
|
||||||
|
<<"type">> => Type,
|
||||||
|
<<"name">> => Name,
|
||||||
|
<<"server">> => <<"127.0.0.1:1883">>,
|
||||||
|
<<"username">> => <<"test">>,
|
||||||
|
<<"password">> => Password,
|
||||||
|
<<"ingress">> =>
|
||||||
|
#{<<"remote">> => #{<<"topic">> => <<"t/#">>}}
|
||||||
|
},
|
||||||
|
|
||||||
|
{ok, 201, _} = request(
|
||||||
|
post,
|
||||||
|
uri(["bridges"]),
|
||||||
|
Template
|
||||||
|
),
|
||||||
|
|
||||||
|
%% update with redacted config
|
||||||
|
Conf = emqx_misc:redact(Template),
|
||||||
|
BridgeID = emqx_bridge_resource:bridge_id(Type, Name),
|
||||||
|
{ok, 200, _ResBin} = request(
|
||||||
|
put,
|
||||||
|
uri(["bridges", BridgeID]),
|
||||||
|
Conf
|
||||||
|
),
|
||||||
|
RawConf = emqx:get_raw_config([bridges, Type, Name]),
|
||||||
|
Value = maps:get(<<"password">>, RawConf),
|
||||||
|
?assertEqual(Password, Value),
|
||||||
|
ok.
|
||||||
|
|
||||||
request(Method, Url, Body) ->
|
request(Method, Url, Body) ->
|
||||||
request(<<"bridge_admin">>, Method, Url, Body).
|
request(<<"bridge_admin">>, Method, Url, Body).
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
`env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`.
|
`env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`.
|
||||||
Now it's possible to set it without quote as `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`.
|
Now it's possible to set it without quote as `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`.
|
||||||
|
|
||||||
|
- Obfuscated sensitive data in the response when querying `bridges` information by API [#9593](https://github.com/emqx/emqx/pull/9593/).
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
- Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. [#9641](https://github.com/emqx/emqx/pull/9641)
|
- Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. [#9641](https://github.com/emqx/emqx/pull/9641)
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
`env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`。
|
`env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`。
|
||||||
此修复后,可以不使用引号,例如 `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`。
|
此修复后,可以不使用引号,例如 `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`。
|
||||||
|
|
||||||
|
- 通过 API 查询 `bridges` 信息时将混淆响应中的敏感数据 [#9593](https://github.com/emqx/emqx/pull/9593/)。
|
||||||
|
|
||||||
## 修复
|
## 修复
|
||||||
|
|
||||||
- 修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 [#9640](https://github.com/emqx/emqx/pull/9640)
|
- 修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 [#9640](https://github.com/emqx/emqx/pull/9640)
|
||||||
|
|
Loading…
Reference in New Issue