feat(message validation): forbid repeated schema checks

Fixes https://emqx.atlassian.net/browse/EMQX-12054
This commit is contained in:
Thales Macedo Garitezi 2024-03-20 15:26:00 -03:00
parent b8cd1c9020
commit 62030e8942
3 changed files with 181 additions and 6 deletions

View File

@ -91,12 +91,7 @@ fields(validation) ->
#{
required => true,
desc => ?DESC("checks"),
validator => fun
([]) ->
{error, "at least one check must be defined"};
(_) ->
ok
end
validator => fun validate_unique_schema_checks/1
}
)}
];
@ -232,3 +227,45 @@ do_validate_unique_names([#{<<"name">> := Name} | _Rest], Acc) when is_map_key(N
{error, <<"duplicated name: ", Name/binary>>};
do_validate_unique_names([#{<<"name">> := Name} | Rest], Acc) ->
do_validate_unique_names(Rest, Acc#{Name => true}).
validate_unique_schema_checks([]) ->
{error, "at least one check must be defined"};
validate_unique_schema_checks(Checks) ->
Seen = sets:new([{version, 2}]),
Duplicated = sets:new([{version, 2}]),
do_validate_unique_schema_checks(Checks, Seen, Duplicated).
do_validate_unique_schema_checks(_Checks = [], _Seen, Duplicated) ->
case sets:to_list(Duplicated) of
[] ->
ok;
DuplicatedChecks0 ->
DuplicatedChecks =
lists:map(
fun({Type, SerdeName}) ->
[atom_to_binary(Type), ":", SerdeName]
end,
DuplicatedChecks0
),
Msg = iolist_to_binary([
<<"duplicated schema checks: ">>,
lists:join(", ", DuplicatedChecks)
]),
{error, Msg}
end;
do_validate_unique_schema_checks(
[#{<<"type">> := Type, <<"schema">> := SerdeName} | Rest],
Seen0,
Duplicated0
) ->
Check = {Type, SerdeName},
case sets:is_element(Check, Seen0) of
true ->
Duplicated = sets:add_element(Check, Duplicated0),
do_validate_unique_schema_checks(Rest, Seen0, Duplicated);
false ->
Seen = sets:add_element(Check, Seen0),
do_validate_unique_schema_checks(Rest, Seen, Duplicated0)
end;
do_validate_unique_schema_checks([_Check | Rest], Seen, Duplicated) ->
do_validate_unique_schema_checks(Rest, Seen, Duplicated).

View File

@ -661,6 +661,21 @@ t_enable_disable_via_api_endpoint(_Config) ->
ok.
t_duplicated_schema_checks(_Config) ->
Name1 = <<"foo">>,
SerdeName = <<"myserde">>,
Check = schema_check(json, SerdeName),
Validation1 = validation(Name1, [Check, sql_check(), Check]),
?assertMatch({400, _}, insert(Validation1)),
Validation2 = validation(Name1, [Check, sql_check()]),
?assertMatch({201, _}, insert(Validation2)),
?assertMatch({400, _}, update(Validation1)),
ok.
%% Check the `all_pass' strategy
t_all_pass(_Config) ->
Name1 = <<"foo">>,

View File

@ -52,6 +52,18 @@ sql_check(SQL) ->
<<"sql">> => SQL
}.
schema_check(Type, SerdeName) ->
schema_check(Type, SerdeName, _Overrides = #{}).
schema_check(Type, SerdeName, Overrides) ->
emqx_utils_maps:deep_merge(
#{
<<"type">> => emqx_utils_conv:bin(Type),
<<"schema">> => SerdeName
},
Overrides
).
eval_sql(Message, SQL) ->
{ok, Check} = emqx_message_validation:parse_sql_check(SQL),
Validation = #{log_failure => #{level => warning}, name => <<"validation">>},
@ -217,3 +229,114 @@ check_test_() ->
{"never passes 2", ?_assertNot(eval_sql(message(), <<"select * where 1 = 2">>))},
{"never passes 3", ?_assertNot(eval_sql(message(), <<"select * where true and false">>))}
].
duplicated_check_test_() ->
[
{"duplicated sql checks are not checked",
?_assertMatch(
[#{<<"checks">> := [_, _]}],
parse_and_check([
validation(<<"foo">>, [sql_check(), sql_check()])
])
)},
{"different serdes with same name",
?_assertMatch(
[#{<<"checks">> := [_, _, _]}],
parse_and_check([
validation(<<"foo">>, [
schema_check(json, <<"a">>),
schema_check(avro, <<"a">>),
schema_check(
protobuf,
<<"a">>,
#{<<"message_name">> => <<"a">>}
)
])
])
)},
{"duplicated serdes 1",
?_assertThrow(
{_Schema, [
#{
reason := <<"duplicated schema checks: json:a">>,
kind := validation_error,
path := "message_validation.validations.1.checks"
}
]},
parse_and_check([
validation(<<"foo">>, [
schema_check(json, <<"a">>),
schema_check(json, <<"a">>)
])
])
)},
{"duplicated serdes 2",
?_assertThrow(
{_Schema, [
#{
reason := <<"duplicated schema checks: json:a">>,
kind := validation_error,
path := "message_validation.validations.1.checks"
}
]},
parse_and_check([
validation(<<"foo">>, [
schema_check(json, <<"a">>),
sql_check(),
schema_check(json, <<"a">>)
])
])
)},
{"duplicated serdes 3",
?_assertThrow(
{_Schema, [
#{
reason := <<"duplicated schema checks: json:a">>,
kind := validation_error,
path := "message_validation.validations.1.checks"
}
]},
parse_and_check([
validation(<<"foo">>, [
schema_check(json, <<"a">>),
schema_check(json, <<"a">>),
sql_check()
])
])
)},
{"duplicated serdes 4",
?_assertThrow(
{_Schema, [
#{
reason := <<"duplicated schema checks: json:a">>,
kind := validation_error,
path := "message_validation.validations.1.checks"
}
]},
parse_and_check([
validation(<<"foo">>, [
schema_check(json, <<"a">>),
schema_check(json, <<"a">>),
schema_check(json, <<"a">>)
])
])
)},
{"duplicated serdes 4",
?_assertThrow(
{_Schema, [
#{
reason := <<"duplicated schema checks: ", _/binary>>,
kind := validation_error,
path := "message_validation.validations.1.checks"
}
]},
parse_and_check([
validation(<<"foo">>, [
schema_check(json, <<"a">>),
schema_check(json, <<"a">>),
schema_check(avro, <<"b">>),
schema_check(avro, <<"b">>)
])
])
)}
].