fix(http connector): deobfuscate sensitive headers
Fixes https://emqx.atlassian.net/browse/EMQX-12213
This commit is contained in:
parent
1e64e531f0
commit
a847389159
|
@ -173,6 +173,11 @@ source_hookpoint(Config) ->
|
||||||
BridgeId = emqx_bridge_resource:bridge_id(Type, Name),
|
BridgeId = emqx_bridge_resource:bridge_id(Type, Name),
|
||||||
emqx_bridge_v2:source_hookpoint(BridgeId).
|
emqx_bridge_v2:source_hookpoint(BridgeId).
|
||||||
|
|
||||||
|
action_hookpoint(Config) ->
|
||||||
|
#{kind := action, type := Type, name := Name} = get_common_values(Config),
|
||||||
|
BridgeId = emqx_bridge_resource:bridge_id(Type, Name),
|
||||||
|
emqx_bridge_resource:bridge_hookpoint(BridgeId).
|
||||||
|
|
||||||
add_source_hookpoint(Config) ->
|
add_source_hookpoint(Config) ->
|
||||||
Hookpoint = source_hookpoint(Config),
|
Hookpoint = source_hookpoint(Config),
|
||||||
ok = emqx_hooks:add(Hookpoint, {?MODULE, source_hookpoint_callback, [self()]}, 1000),
|
ok = emqx_hooks:add(Hookpoint, {?MODULE, source_hookpoint_callback, [self()]}, 1000),
|
||||||
|
@ -378,6 +383,14 @@ start_connector_api(ConnectorName, ConnectorType) ->
|
||||||
ct:pal("connector update (http) result:\n ~p", [Res]),
|
ct:pal("connector update (http) result:\n ~p", [Res]),
|
||||||
Res.
|
Res.
|
||||||
|
|
||||||
|
get_connector_api(ConnectorType, ConnectorName) ->
|
||||||
|
ConnectorId = emqx_connector_resource:connector_id(ConnectorType, ConnectorName),
|
||||||
|
Path = emqx_mgmt_api_test_util:api_path(["connectors", ConnectorId]),
|
||||||
|
ct:pal("get connector ~s (http)", [ConnectorId]),
|
||||||
|
Res = request(get, Path, _Params = []),
|
||||||
|
ct:pal("get connector (http) result:\n ~p", [Res]),
|
||||||
|
Res.
|
||||||
|
|
||||||
create_action_api(Config) ->
|
create_action_api(Config) ->
|
||||||
create_action_api(Config, _Overrides = #{}).
|
create_action_api(Config, _Overrides = #{}).
|
||||||
|
|
||||||
|
|
|
@ -69,10 +69,21 @@ end_per_suite(Config) ->
|
||||||
suite() ->
|
suite() ->
|
||||||
[{timetrap, {seconds, 60}}].
|
[{timetrap, {seconds, 60}}].
|
||||||
|
|
||||||
|
init_per_testcase(t_update_with_sensitive_data, Config) ->
|
||||||
|
HTTPPath = <<"/foo/bar">>,
|
||||||
|
ServerSSLOpts = false,
|
||||||
|
{ok, {HTTPPort, _Pid}} = emqx_bridge_http_connector_test_server:start_link(
|
||||||
|
_Port = random, HTTPPath, ServerSSLOpts
|
||||||
|
),
|
||||||
|
ok = emqx_bridge_http_connector_test_server:set_handler(success_handler()),
|
||||||
|
[{path, HTTPPath}, {http_server, #{port => HTTPPort, path => HTTPPath}} | Config];
|
||||||
init_per_testcase(_TestCase, Config) ->
|
init_per_testcase(_TestCase, Config) ->
|
||||||
Server = start_http_server(#{response_delay_ms => 0}),
|
Server = start_http_server(#{response_delay_ms => 0}),
|
||||||
[{http_server, Server} | Config].
|
[{http_server, Server} | Config].
|
||||||
|
|
||||||
|
end_per_testcase(t_update_with_sensitive_data, Config) ->
|
||||||
|
ok = emqx_bridge_http_connector_test_server:stop(),
|
||||||
|
end_per_testcase(common, proplists:delete(http_server, Config));
|
||||||
end_per_testcase(_TestCase, Config) ->
|
end_per_testcase(_TestCase, Config) ->
|
||||||
case ?config(http_server, Config) of
|
case ?config(http_server, Config) of
|
||||||
undefined -> ok;
|
undefined -> ok;
|
||||||
|
@ -112,6 +123,69 @@ t_compose_connector_url_and_action_path(Config) ->
|
||||||
),
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
%% Checks that we can successfully update a connector containing sensitive headers and
|
||||||
|
%% they won't be clobbered by the update.
|
||||||
|
t_update_with_sensitive_data(Config) ->
|
||||||
|
?check_trace(
|
||||||
|
begin
|
||||||
|
ConnectorCfg0 = make_connector_config(Config),
|
||||||
|
AuthHeader = <<"Bearer some_token">>,
|
||||||
|
ConnectorCfg1 = emqx_utils_maps:deep_merge(
|
||||||
|
ConnectorCfg0,
|
||||||
|
#{<<"headers">> => #{<<"authorization">> => AuthHeader}}
|
||||||
|
),
|
||||||
|
ActionCfg = make_action_config(Config),
|
||||||
|
CreateConfig = [
|
||||||
|
{bridge_kind, action},
|
||||||
|
{action_type, ?BRIDGE_TYPE},
|
||||||
|
{action_name, ?BRIDGE_NAME},
|
||||||
|
{action_config, ActionCfg},
|
||||||
|
{connector_type, ?BRIDGE_TYPE},
|
||||||
|
{connector_name, ?CONNECTOR_NAME},
|
||||||
|
{connector_config, ConnectorCfg1}
|
||||||
|
],
|
||||||
|
{ok, {{_, 201, _}, _, #{<<"headers">> := #{<<"authorization">> := Obfuscated}}}} =
|
||||||
|
emqx_bridge_v2_testlib:create_connector_api(CreateConfig),
|
||||||
|
{ok, _} =
|
||||||
|
emqx_bridge_v2_testlib:create_kind_api(CreateConfig),
|
||||||
|
BridgeId = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
||||||
|
{ok, _} = emqx_bridge_v2_testlib:create_rule_api(
|
||||||
|
#{
|
||||||
|
sql => <<"select * from \"t/http\" ">>,
|
||||||
|
actions => [BridgeId]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
emqx:publish(emqx_message:make(<<"t/http">>, <<"1">>)),
|
||||||
|
?assertReceive({http, #{<<"authorization">> := AuthHeader}, _}),
|
||||||
|
|
||||||
|
%% Now update the connector and see if the header stays deobfuscated. We send the old
|
||||||
|
%% auth header as an obfuscated value to simulate the behavior of the frontend.
|
||||||
|
ConnectorCfg2 = emqx_utils_maps:deep_merge(
|
||||||
|
ConnectorCfg1,
|
||||||
|
#{
|
||||||
|
<<"headers">> => #{
|
||||||
|
<<"authorization">> => Obfuscated,
|
||||||
|
<<"other_header">> => <<"new">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{ok, _} = emqx_bridge_v2_testlib:update_connector_api(
|
||||||
|
?CONNECTOR_NAME,
|
||||||
|
?BRIDGE_TYPE,
|
||||||
|
ConnectorCfg2
|
||||||
|
),
|
||||||
|
|
||||||
|
emqx:publish(emqx_message:make(<<"t/http">>, <<"2">>)),
|
||||||
|
%% Should not be obfuscated.
|
||||||
|
?assertReceive({http, #{<<"authorization">> := AuthHeader}, _}, 2_000),
|
||||||
|
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
|
||||||
|
ok.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% helpers
|
%% helpers
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -123,7 +197,10 @@ make_connector_config(Config) ->
|
||||||
<<"url">> => iolist_to_binary(io_lib:format("http://localhost:~p", [Port])),
|
<<"url">> => iolist_to_binary(io_lib:format("http://localhost:~p", [Port])),
|
||||||
<<"headers">> => #{},
|
<<"headers">> => #{},
|
||||||
<<"pool_type">> => <<"hash">>,
|
<<"pool_type">> => <<"hash">>,
|
||||||
<<"pool_size">> => 1
|
<<"pool_size">> => 1,
|
||||||
|
<<"resource_opts">> => #{
|
||||||
|
<<"health_check_interval">> => <<"100ms">>
|
||||||
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
make_action_config(Config) ->
|
make_action_config(Config) ->
|
||||||
|
@ -136,5 +213,22 @@ make_action_config(Config) ->
|
||||||
<<"method">> => <<"post">>,
|
<<"method">> => <<"post">>,
|
||||||
<<"headers">> => #{},
|
<<"headers">> => #{},
|
||||||
<<"body">> => <<"${.}">>
|
<<"body">> => <<"${.}">>
|
||||||
|
},
|
||||||
|
<<"resource_opts">> => #{
|
||||||
|
<<"health_check_interval">> => <<"100ms">>
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
success_handler() ->
|
||||||
|
TestPid = self(),
|
||||||
|
fun(Req0, State) ->
|
||||||
|
{ok, Body, Req} = cowboy_req:read_body(Req0),
|
||||||
|
TestPid ! {http, cowboy_req:headers(Req), Body},
|
||||||
|
Rep = cowboy_req:reply(
|
||||||
|
200,
|
||||||
|
#{<<"content-type">> => <<"application/json">>},
|
||||||
|
<<"{}">>,
|
||||||
|
Req
|
||||||
|
),
|
||||||
|
{ok, Rep, State}
|
||||||
|
end.
|
||||||
|
|
|
@ -152,19 +152,24 @@ redact_v(_V) ->
|
||||||
?REDACT_VAL.
|
?REDACT_VAL.
|
||||||
|
|
||||||
deobfuscate(NewConf, OldConf) ->
|
deobfuscate(NewConf, OldConf) ->
|
||||||
|
deobfuscate(NewConf, OldConf, fun(_) -> false end).
|
||||||
|
|
||||||
|
deobfuscate(NewConf, OldConf, IsSensitiveFun) ->
|
||||||
maps:fold(
|
maps:fold(
|
||||||
fun(K, V, Acc) ->
|
fun(K, V, Acc) ->
|
||||||
case maps:find(K, OldConf) of
|
case maps:find(K, OldConf) of
|
||||||
error ->
|
error ->
|
||||||
case is_redacted(K, V) of
|
case is_redacted(K, V, IsSensitiveFun) of
|
||||||
%% don't put redacted value into new config
|
%% don't put redacted value into new config
|
||||||
true -> Acc;
|
true -> Acc;
|
||||||
false -> Acc#{K => V}
|
false -> Acc#{K => V}
|
||||||
end;
|
end;
|
||||||
|
{ok, OldV} when is_map(V), is_map(OldV), ?IS_KEY_HEADERS(K) ->
|
||||||
|
Acc#{K => deobfuscate(V, OldV, fun check_is_sensitive_header/1)};
|
||||||
{ok, OldV} when is_map(V), is_map(OldV) ->
|
{ok, OldV} when is_map(V), is_map(OldV) ->
|
||||||
Acc#{K => deobfuscate(V, OldV)};
|
Acc#{K => deobfuscate(V, OldV, IsSensitiveFun)};
|
||||||
{ok, OldV} ->
|
{ok, OldV} ->
|
||||||
case is_redacted(K, V) of
|
case is_redacted(K, V, IsSensitiveFun) of
|
||||||
true ->
|
true ->
|
||||||
Acc#{K => OldV};
|
Acc#{K => OldV};
|
||||||
_ ->
|
_ ->
|
||||||
|
@ -280,6 +285,11 @@ deobfuscate_test() ->
|
||||||
%% Don't have password before and should allow put non-redact-val into new config
|
%% Don't have password before and should allow put non-redact-val into new config
|
||||||
NewConf3 = #{foo => <<"bar3">>, password => <<"123456">>},
|
NewConf3 = #{foo => <<"bar3">>, password => <<"123456">>},
|
||||||
?assertEqual(NewConf3, deobfuscate(NewConf3, #{foo => <<"bar">>})),
|
?assertEqual(NewConf3, deobfuscate(NewConf3, #{foo => <<"bar">>})),
|
||||||
|
|
||||||
|
HeaderConf1 = #{<<"headers">> => #{<<"Authorization">> => <<"Bearer token">>}},
|
||||||
|
HeaderConf1Obs = #{<<"headers">> => #{<<"Authorization">> => ?REDACT_VAL}},
|
||||||
|
?assertEqual(HeaderConf1, deobfuscate(HeaderConf1Obs, HeaderConf1)),
|
||||||
|
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
redact_header_test_() ->
|
redact_header_test_() ->
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed an issue where sensitive HTTP header values like `Authorization` would be substituted by `******` after updating a connector.
|
Loading…
Reference in New Issue