Merge pull request #13264 from thalesmg/fix-postgres-disabled-prepared-r57-20240614

fix(postgres): authn/authz/batch requests when prepared statements are disabled
This commit is contained in:
Thales Macedo Garitezi 2024-06-17 11:05:05 -03:00 committed by GitHub
commit fb492e3dc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 191 additions and 64 deletions

View File

@ -30,15 +30,28 @@
-define(PATH, [authentication]). -define(PATH, [authentication]).
-import(emqx_common_test_helpers, [on_exit/1]).
all() -> all() ->
AllTCs = emqx_common_test_helpers:all(?MODULE),
TCs = AllTCs -- require_seeds_tests(),
[ [
{group, require_seeds}, {group, require_seeds}
t_update_with_invalid_config, | TCs
t_update_with_bad_config_value
]. ].
groups() -> groups() ->
[{require_seeds, [], [t_create, t_authenticate, t_update, t_destroy, t_is_superuser]}]. [{require_seeds, [], require_seeds_tests()}].
require_seeds_tests() ->
[
t_create,
t_authenticate,
t_authenticate_disabled_prepared_statements,
t_update,
t_destroy,
t_is_superuser
].
init_per_testcase(_, Config) -> init_per_testcase(_, Config) ->
emqx_authn_test_lib:delete_authenticators( emqx_authn_test_lib:delete_authenticators(
@ -47,6 +60,10 @@ init_per_testcase(_, Config) ->
), ),
Config. Config.
end_per_testcase(_TestCase, _Config) ->
emqx_common_test_helpers:call_janitor(),
ok.
init_per_group(require_seeds, Config) -> init_per_group(require_seeds, Config) ->
ok = init_seeds(), ok = init_seeds(),
Config. Config.
@ -70,7 +87,12 @@ init_per_suite(Config) ->
), ),
[{apps, Apps} | Config]; [{apps, Apps} | Config];
false -> false ->
{skip, no_pgsql} case os:getenv("IS_CI") of
"yes" ->
throw(no_postgres);
_ ->
{skip, no_postgres}
end
end. end.
end_per_suite(Config) -> end_per_suite(Config) ->
@ -174,6 +196,25 @@ test_user_auth(#{
?GLOBAL ?GLOBAL
). ).
t_authenticate_disabled_prepared_statements(Config) ->
ResConfig = maps:merge(pgsql_config(), #{disable_prepared_statements => true}),
{ok, _} = emqx_resource:recreate_local(?PGSQL_RESOURCE, emqx_postgresql, ResConfig),
on_exit(fun() ->
emqx_resource:recreate_local(?PGSQL_RESOURCE, emqx_postgresql, pgsql_config())
end),
ok = lists:foreach(
fun(Sample0) ->
Sample = maps:update_with(
config_params,
fun(Cfg) -> Cfg#{<<"disable_prepared_statements">> => true} end,
Sample0
),
ct:pal("test_user_auth sample: ~p", [Sample]),
test_user_auth(Sample)
end,
user_seeds()
).
t_destroy(_Config) -> t_destroy(_Config) ->
AuthConfig = raw_pgsql_auth_config(), AuthConfig = raw_pgsql_auth_config(),

View File

@ -601,7 +601,7 @@ t_simple_sql_query(Config) ->
{ok, _}, {ok, _},
create_bridge(Config) create_bridge(Config)
), ),
Request = {sql, <<"SELECT count(1) AS T">>}, Request = {query, <<"SELECT count(1) AS T">>},
Result = Result =
case QueryMode of case QueryMode of
sync -> sync ->
@ -651,7 +651,7 @@ t_bad_sql_parameter(Config) ->
{ok, _}, {ok, _},
create_bridge(Config) create_bridge(Config)
), ),
Request = {sql, <<"">>, [bad_parameter]}, Request = {query, <<"">>, [bad_parameter]},
Result = Result =
case QueryMode of case QueryMode of
sync -> sync ->

View File

@ -102,6 +102,18 @@ init_per_group(Group, Config) when
{connector_type, group_to_type(Group)} {connector_type, group_to_type(Group)}
| Config | Config
]; ];
init_per_group(batch_enabled, Config) ->
[
{batch_size, 10},
{batch_time, <<"10ms">>}
| Config
];
init_per_group(batch_disabled, Config) ->
[
{batch_size, 1},
{batch_time, <<"0ms">>}
| Config
];
init_per_group(_Group, Config) -> init_per_group(_Group, Config) ->
Config. Config.
@ -262,17 +274,68 @@ t_start_action_or_source_with_disabled_connector(Config) ->
ok. ok.
t_disable_prepared_statements(matrix) -> t_disable_prepared_statements(matrix) ->
[[postgres], [timescale], [matrix]]; [
[postgres, batch_disabled],
[postgres, batch_enabled],
[timescale, batch_disabled],
[timescale, batch_enabled],
[matrix, batch_disabled],
[matrix, batch_enabled]
];
t_disable_prepared_statements(Config0) -> t_disable_prepared_statements(Config0) ->
BatchSize = ?config(batch_size, Config0),
BatchTime = ?config(batch_time, Config0),
ConnectorConfig0 = ?config(connector_config, Config0), ConnectorConfig0 = ?config(connector_config, Config0),
ConnectorConfig = maps:merge(ConnectorConfig0, #{<<"disable_prepared_statements">> => true}), ConnectorConfig = maps:merge(ConnectorConfig0, #{<<"disable_prepared_statements">> => true}),
Config = lists:keyreplace(connector_config, 1, Config0, {connector_config, ConnectorConfig}), BridgeConfig0 = ?config(bridge_config, Config0),
ok = emqx_bridge_v2_testlib:t_sync_query( BridgeConfig = emqx_utils_maps:deep_merge(
Config, BridgeConfig0,
fun make_message/0, #{
fun(Res) -> ?assertMatch({ok, _}, Res) end, <<"resource_opts">> => #{
postgres_bridge_connector_on_query_return <<"batch_size">> => BatchSize,
<<"batch_time">> => BatchTime,
<<"query_mode">> => <<"async">>
}
}
), ),
Config1 = lists:keyreplace(connector_config, 1, Config0, {connector_config, ConnectorConfig}),
Config = lists:keyreplace(bridge_config, 1, Config1, {bridge_config, BridgeConfig}),
?check_trace(
#{timetrap => 5_000},
begin
?assertMatch({ok, _}, emqx_bridge_v2_testlib:create_bridge_api(Config)),
RuleTopic = <<"t/postgres">>,
Type = ?config(bridge_type, Config),
{ok, _} = emqx_bridge_v2_testlib:create_rule_and_action_http(Type, RuleTopic, Config),
ResourceId = emqx_bridge_v2_testlib:resource_id(Config),
?retry(
_Sleep = 1_000,
_Attempts = 20,
?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceId))
),
{ok, C} = emqtt:start_link(),
{ok, _} = emqtt:connect(C),
lists:foreach(
fun(N) ->
emqtt:publish(C, RuleTopic, integer_to_binary(N))
end,
lists:seq(1, BatchSize)
),
case BatchSize > 1 of
true ->
?block_until(#{
?snk_kind := "postgres_success_batch_result",
row_count := BatchSize
}),
ok;
false ->
ok
end,
ok
end,
[]
),
emqx_bridge_v2_testlib:delete_all_bridges_and_connectors(),
ok = emqx_bridge_v2_testlib:t_on_get_status(Config, #{failure_status => connecting}), ok = emqx_bridge_v2_testlib:t_on_get_status(Config, #{failure_status => connecting}),
emqx_bridge_v2_testlib:delete_all_bridges_and_connectors(), emqx_bridge_v2_testlib:delete_all_bridges_and_connectors(),
ok = emqx_bridge_v2_testlib:t_create_via_http(Config), ok = emqx_bridge_v2_testlib:t_create_via_http(Config),

View File

@ -59,6 +59,9 @@
default_port => ?PGSQL_DEFAULT_PORT default_port => ?PGSQL_DEFAULT_PORT
}). }).
-type connector_resource_id() :: binary().
-type action_resource_id() :: binary().
-type template() :: {unicode:chardata(), emqx_template_sql:row_template()}. -type template() :: {unicode:chardata(), emqx_template_sql:row_template()}.
-type state() :: -type state() ::
#{ #{
@ -319,38 +322,40 @@ do_check_channel_sql(
on_get_channels(ResId) -> on_get_channels(ResId) ->
emqx_bridge_v2:get_channels_for_connector(ResId). emqx_bridge_v2:get_channels_for_connector(ResId).
on_query(InstId, {TypeOrKey, NameOrSQL}, State) -> -spec on_query
on_query(InstId, {TypeOrKey, NameOrSQL, []}, State); %% Called from authn and authz modules
(connector_resource_id(), {prepared_query, binary(), [term()]}, state()) ->
{ok, _} | {error, term()};
%% Called from bridges
(connector_resource_id(), {action_resource_id(), map()}, state()) ->
{ok, _} | {error, term()}.
on_query(InstId, {TypeOrKey, NameOrMap}, State) ->
on_query(InstId, {TypeOrKey, NameOrMap, []}, State);
on_query( on_query(
InstId, InstId,
{TypeOrKey, NameOrSQL, Params}, {TypeOrKey, NameOrMap, Params},
#{pool_name := PoolName} = State #{pool_name := PoolName} = State
) -> ) ->
?SLOG(debug, #{ ?SLOG(debug, #{
msg => "postgresql_connector_received_sql_query", msg => "postgresql_connector_received_sql_query",
connector => InstId, connector => InstId,
type => TypeOrKey, type => TypeOrKey,
sql => NameOrSQL, sql => NameOrMap,
state => State state => State
}), }),
Type = pgsql_query_type(TypeOrKey, State), {QueryType, NameOrSQL2, Data} = proc_sql_params(TypeOrKey, NameOrMap, Params, State),
{NameOrSQL2, Data} = proc_sql_params(TypeOrKey, NameOrSQL, Params, State), emqx_trace:rendered_action_template(
Res = on_sql_query(TypeOrKey, InstId, PoolName, Type, NameOrSQL2, Data), TypeOrKey,
#{
statement_type => QueryType,
statement_or_name => NameOrSQL2,
data => Data
}
),
Res = on_sql_query(InstId, PoolName, QueryType, NameOrSQL2, Data),
?tp(postgres_bridge_connector_on_query_return, #{instance_id => InstId, result => Res}), ?tp(postgres_bridge_connector_on_query_return, #{instance_id => InstId, result => Res}),
handle_result(Res). handle_result(Res).
pgsql_query_type(_TypeOrTag, #{prepares := disabled}) ->
query;
pgsql_query_type(sql, _ConnectorState) ->
query;
pgsql_query_type(query, _ConnectorState) ->
query;
pgsql_query_type(prepared_query, _ConnectorState) ->
prepared_query;
%% for bridge
pgsql_query_type(_, ConnectorState) ->
pgsql_query_type(prepared_query, ConnectorState).
on_batch_query( on_batch_query(
InstId, InstId,
[{Key, _} = Request | _] = BatchReq, [{Key, _} = Request | _] = BatchReq,
@ -370,7 +375,15 @@ on_batch_query(
{_Statement, RowTemplate} -> {_Statement, RowTemplate} ->
StatementTemplate = get_templated_statement(BinKey, State), StatementTemplate = get_templated_statement(BinKey, State),
Rows = [render_prepare_sql_row(RowTemplate, Data) || {_Key, Data} <- BatchReq], Rows = [render_prepare_sql_row(RowTemplate, Data) || {_Key, Data} <- BatchReq],
case on_sql_query(Key, InstId, PoolName, execute_batch, StatementTemplate, Rows) of emqx_trace:rendered_action_template(
Key,
#{
statement_type => execute_batch,
statement_or_name => StatementTemplate,
data => Rows
}
),
case on_sql_query(InstId, PoolName, execute_batch, StatementTemplate, Rows) of
{error, _Error} = Result -> {error, _Error} = Result ->
handle_result(Result); handle_result(Result);
{_Column, Results} -> {_Column, Results} ->
@ -386,25 +399,38 @@ on_batch_query(InstId, BatchReq, State) ->
}), }),
{error, {unrecoverable_error, invalid_request}}. {error, {unrecoverable_error, invalid_request}}.
proc_sql_params(query, SQLOrKey, Params, _State) -> proc_sql_params(ActionResId, #{} = Map, [], State) when is_binary(ActionResId) ->
{SQLOrKey, Params}; %% When this connector is called from actions/bridges.
proc_sql_params(prepared_query, SQLOrKey, Params, _State) -> DisablePreparedStatements = prepared_statements_disabled(State),
{SQLOrKey, Params}; {ExprTemplate, RowTemplate} = get_template(ActionResId, State),
proc_sql_params(TypeOrKey, SQLOrData, Params, State) -> Rendered = render_prepare_sql_row(RowTemplate, Map),
DisablePreparedStatements = maps:get(prepares, State, #{}) =:= disabled, case DisablePreparedStatements of
BinKey = to_bin(TypeOrKey), true ->
case get_template(BinKey, State) of {query, ExprTemplate, Rendered};
undefined -> false ->
{SQLOrData, Params}; {prepared_query, ActionResId, Rendered}
{Statement, RowTemplate} -> end;
Rendered = render_prepare_sql_row(RowTemplate, SQLOrData), proc_sql_params(prepared_query, ConnResId, Params, State) ->
case DisablePreparedStatements of %% When this connector is called from authn/authz modules
true -> DisablePreparedStatements = prepared_statements_disabled(State),
{Statement, Rendered}; case DisablePreparedStatements of
false -> true ->
{BinKey, Rendered} #{query_templates := #{ConnResId := {ExprTemplate, _VarsTemplate}}} = State,
end {query, ExprTemplate, Params};
end. false ->
%% Connector resource id itself is the prepared statement name
{prepared_query, ConnResId, Params}
end;
proc_sql_params(QueryType, SQL, Params, _State) when
is_atom(QueryType) andalso
(is_binary(SQL) orelse is_list(SQL)) andalso
is_list(Params)
->
%% When called to do ad-hoc commands/queries.
{QueryType, SQL, Params}.
prepared_statements_disabled(State) ->
maps:get(prepares, State, #{}) =:= disabled.
get_template(Key, #{installed_channels := Channels} = _State) when is_map_key(Key, Channels) -> get_template(Key, #{installed_channels := Channels} = _State) when is_map_key(Key, Channels) ->
BinKey = to_bin(Key), BinKey = to_bin(Key),
@ -420,21 +446,17 @@ get_templated_statement(Key, #{installed_channels := Channels} = _State) when
-> ->
BinKey = to_bin(Key), BinKey = to_bin(Key),
ChannelState = maps:get(BinKey, Channels), ChannelState = maps:get(BinKey, Channels),
ChannelPreparedStatements = maps:get(prepares, ChannelState), case ChannelState of
maps:get(BinKey, ChannelPreparedStatements); #{prepares := disabled, query_templates := #{BinKey := {ExprTemplate, _}}} ->
ExprTemplate;
#{prepares := #{BinKey := ExprTemplate}} ->
ExprTemplate
end;
get_templated_statement(Key, #{prepares := PrepStatements}) -> get_templated_statement(Key, #{prepares := PrepStatements}) ->
BinKey = to_bin(Key), BinKey = to_bin(Key),
maps:get(BinKey, PrepStatements). maps:get(BinKey, PrepStatements).
on_sql_query(Key, InstId, PoolName, Type, NameOrSQL, Data) -> on_sql_query(InstId, PoolName, Type, NameOrSQL, Data) ->
emqx_trace:rendered_action_template(
Key,
#{
statement_type => Type,
statement_or_name => NameOrSQL,
data => Data
}
),
try ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Data]}, no_handover) of try ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Data]}, no_handover) of
{error, Reason} -> {error, Reason} ->
?tp( ?tp(
@ -785,6 +807,7 @@ handle_batch_result([{error, Error} | _Rest], _Acc) ->
TranslatedError = translate_to_log_context(Error), TranslatedError = translate_to_log_context(Error),
{error, {unrecoverable_error, export_error(TranslatedError)}}; {error, {unrecoverable_error, export_error(TranslatedError)}};
handle_batch_result([], Acc) -> handle_batch_result([], Acc) ->
?tp("postgres_success_batch_result", #{row_count => Acc}),
{ok, Acc}. {ok, Acc}.
translate_to_log_context({error, Reason}) -> translate_to_log_context({error, Reason}) ->