Bladeren bron

Merge pull request #13792 from lafirest/feat/banned_api

feat(banned): supports querying the banned with filters
lafirest 1 jaar geleden
bovenliggende
commit
9c50d24027

+ 1 - 1
apps/emqx/include/emqx.hrl

@@ -103,7 +103,7 @@
     by :: binary(),
     reason :: binary(),
     at :: integer(),
-    until :: integer()
+    until :: integer() | infinity
 }).
 
 %%--------------------------------------------------------------------

+ 21 - 15
apps/emqx/src/emqx_banned.erl

@@ -40,6 +40,7 @@
     info/1,
     format/1,
     parse/1,
+    parse_who/1,
     clear/0,
     who/2,
     tables/0
@@ -68,10 +69,6 @@
 -define(BANNED_INDIVIDUAL_TAB, ?MODULE).
 -define(BANNED_RULE_TAB, emqx_banned_rules).
 
-%% The default expiration time should be infinite
-%% but for compatibility, a large number (1 years) is used here to represent the 'infinite'
--define(EXPIRATION_TIME, 31536000).
-
 -ifdef(TEST).
 -compile(export_all).
 -compile(nowarn_export_all).
@@ -149,7 +146,7 @@ parse(Params) ->
             By = maps:get(<<"by">>, Params, <<"mgmt_api">>),
             Reason = maps:get(<<"reason">>, Params, <<"">>),
             At = maps:get(<<"at">>, Params, erlang:system_time(second)),
-            Until = maps:get(<<"until">>, Params, At + ?EXPIRATION_TIME),
+            Until = maps:get(<<"until">>, Params, infinity),
             case Until > erlang:system_time(second) of
                 true ->
                     {ok, #banned{
@@ -341,19 +338,26 @@ parse_stream([], Ok, Error) ->
     {ok, Ok}.
 
 normalize_parse_item(#{<<"as">> := As} = Item) ->
-    ParseTime = fun(Name, Input) ->
-        maybe
-            #{Name := Time} ?= Input,
-            {ok, Epoch} ?= emqx_utils_calendar:to_epoch_second(emqx_utils_conv:str(Time)),
-            {ok, Input#{Name := Epoch}}
-        else
-            {error, _} = Error ->
-                Error;
-            NoTime when is_map(NoTime) ->
-                {ok, NoTime}
+    ToSecond = fun(Name, Time, Input) ->
+        case emqx_utils_calendar:to_epoch_second(emqx_utils_conv:str(Time)) of
+            {ok, Epoch} ->
+                {ok, Input#{Name := Epoch}};
+            Error ->
+                Error
         end
     end,
 
+    ParseTime = fun
+        (<<"at">>, #{<<"at">> := Time} = Input) ->
+            ToSecond(<<"at">>, Time, Input);
+        (<<"until">>, #{<<"until">> := <<"infinity">>} = Input) ->
+            {ok, Input#{<<"until">> := infinity}};
+        (<<"until">>, #{<<"until">> := Time} = Input) ->
+            ToSecond(<<"until">>, Time, Input);
+        (_, Input) ->
+            {ok, Input}
+    end,
+
     maybe
         {ok, Type} ?= emqx_utils:safe_to_existing_atom(As),
         {ok, Item1} ?= ParseTime(<<"at">>, Item#{<<"as">> := Type}),
@@ -468,6 +472,8 @@ format_who({AsRE, {_RE, REOriginal}}) when AsRE =:= clientid_re orelse AsRE =:=
 format_who({As, Who}) when As =:= clientid orelse As =:= username ->
     {As, Who}.
 
+to_rfc3339(infinity) ->
+    infinity;
 to_rfc3339(Timestamp) ->
     emqx_utils_calendar:epoch_to_rfc3339(Timestamp, second).
 

+ 13 - 0
apps/emqx/test/emqx_banned_SUITE.erl

@@ -293,6 +293,19 @@ t_error_bootstrap_file(_) ->
     ?assertMatch(Keys, [element(2, Data) || Data <- get_banned_list()]),
     ok.
 
+t_until_expiry(_) ->
+    Who = #{<<"as">> => clientid, <<"who">> => <<"t_until_expiry">>},
+
+    {ok, Banned} = emqx_banned:parse(Who),
+    {ok, _} = emqx_banned:create(Banned),
+
+    [Data] = emqx_banned:look_up(Who),
+    ?assertEqual(Banned, Data),
+    ?assertMatch(#{until := infinity}, emqx_banned:format(Data)),
+
+    emqx_banned:clear(),
+    ok.
+
 receive_messages(Count) ->
     receive_messages(Count, []).
 receive_messages(0, Msgs) ->

+ 97 - 6
apps/emqx_management/src/emqx_mgmt_api.erl

@@ -31,6 +31,8 @@
 -export([
     node_query/6,
     node_query/7,
+    node_query_with_tabs/6,
+    node_query_with_tabs/7,
     cluster_query/5,
     cluster_query/6,
     b2i/1
@@ -206,6 +208,68 @@ do_node_query(
             end
     end.
 
+%%--------------------------------------------------------------------
+%% Node Query with tables
+%%--------------------------------------------------------------------
+
+-spec node_query_with_tabs(
+    node(),
+    [atom()],
+    query_params(),
+    query_schema(),
+    query_to_match_spec_fun(),
+    format_result_fun()
+) -> {error, page_limit_invalid} | {error, atom(), term()} | query_return().
+node_query_with_tabs(Node, Tabs, QString, QSchema, MsFun, FmtFun) ->
+    node_query_with_tabs(Node, Tabs, QString, QSchema, MsFun, FmtFun, #{}).
+
+-spec node_query_with_tabs(
+    node(),
+    [atom()],
+    query_params(),
+    query_schema(),
+    query_to_match_spec_fun(),
+    format_result_fun(),
+    query_options()
+) -> {error, page_limit_invalid} | {error, atom(), term()} | query_return().
+node_query_with_tabs(Node, [Tab | Tabs], QString, QSchema, MsFun, FmtFun, Options) ->
+    case parse_pager_params(QString) of
+        false ->
+            {error, page_limit_invalid};
+        Meta ->
+            {_CodCnt, NQString} = parse_qstring(QString, QSchema),
+            ResultAcc = init_query_result(),
+            QueryState = init_query_state(Tab, NQString, MsFun, Meta, Options),
+            NResultAcc = do_node_query_with_tabs(Node, Tabs, QueryState, ResultAcc),
+            format_query_result(FmtFun, Meta, NResultAcc)
+    end.
+
+%% @private
+do_node_query_with_tabs(
+    Node,
+    Tabs,
+    QueryState,
+    ResultAcc
+) ->
+    case do_query(Node, QueryState) of
+        {error, Error} ->
+            {error, Node, Error};
+        {Rows, NQueryState = #{complete := Complete}} ->
+            case accumulate_query_rows(Node, Rows, NQueryState, ResultAcc) of
+                {enough, NResultAcc} ->
+                    FComplete = Complete andalso Tabs =:= [],
+                    finalize_query(NResultAcc, mark_complete(NQueryState, FComplete));
+                {more, NResultAcc} when not Complete ->
+                    do_node_query_with_tabs(Node, Tabs, NQueryState, NResultAcc);
+                {more, NResultAcc} when Tabs =/= [] ->
+                    [Tab | NTabs] = Tabs,
+                    NQueryState2 = reinit_query_state(Tab, NQueryState),
+                    do_node_query_with_tabs(Node, NTabs, NQueryState2, NResultAcc);
+                {more, NResultAcc} ->
+                    finalize_query(NResultAcc, NQueryState)
+            end
+    end.
+
 %%--------------------------------------------------------------------
 %% Cluster Query
 %%--------------------------------------------------------------------
@@ -338,6 +402,25 @@ init_query_state(Tab, QString, MsFun, _Meta = #{page := Page, limit := Limit}, O
             QueryState#{total => #{}}
     end.
 
+reinit_query_state(Tab, #{qs := QString, msfun := MsFun} = QueryState) ->
+    #{match_spec := Ms, fuzzy_fun := FuzzyFun} = erlang:apply(MsFun, [Tab, QString]),
+    _ =
+        case FuzzyFun of
+            undefined ->
+                ok;
+            {NamedFun, Args} ->
+                true = is_list(Args),
+                {type, external} = erlang:fun_info(NamedFun, type)
+        end,
+
+    QueryState2 = reset_query_state(QueryState),
+
+    QueryState2#{
+        table := Tab,
+        match_spec := Ms,
+        fuzzy_fun := FuzzyFun
+    }.
+
 reset_query_state(QueryState) ->
     maps:remove(continuation, mark_complete(QueryState, false)).
 
@@ -679,12 +762,20 @@ to_type(V, TargetType) ->
             throw(bad_value_type)
     end.
 
-to_type_(V, atom) -> to_atom(V);
-to_type_(V, integer) -> to_integer(V);
-to_type_(V, timestamp) -> to_timestamp(V);
-to_type_(V, ip) -> to_ip(V);
-to_type_(V, ip_port) -> to_ip_port(V);
-to_type_(V, _) -> V.
+to_type_(V, atom) ->
+    to_atom(V);
+to_type_(V, integer) ->
+    to_integer(V);
+to_type_(V, timestamp) ->
+    to_timestamp(V);
+to_type_(V, ip) ->
+    to_ip(V);
+to_type_(V, ip_port) ->
+    to_ip_port(V);
+to_type_(V, Fun) when is_function(Fun, 1) ->
+    Fun(V);
+to_type_(V, _) ->
+    V.
 
 to_atom(A) when is_atom(A) ->
     A;

+ 192 - 12
apps/emqx_management/src/emqx_mgmt_api_banned.erl

@@ -16,10 +16,12 @@
 
 -module(emqx_mgmt_api_banned).
 
+-feature(maybe_expr, enable).
+
 -include_lib("hocon/include/hoconsc.hrl").
 -include_lib("emqx/include/emqx.hrl").
 -include_lib("typerefl/include/types.hrl").
-
+-include_lib("stdlib/include/ms_transform.hrl").
 -include("emqx_mgmt.hrl").
 
 -behaviour(minirest_api).
@@ -32,7 +34,7 @@
     namespace/0
 ]).
 
--export([format/1]).
+-export([qs2ms/2, run_fuzzy_filter/2, format/1]).
 
 -export([
     banned/2,
@@ -45,6 +47,16 @@
 
 -define(FORMAT_FUN, {?MODULE, format}).
 
+-define(BANNED_QSCHEMA, [
+    {<<"username">>, binary},
+    {<<"clientid">>, binary},
+    {<<"peerhost">>, binary},
+    {<<"like_clientid">>, binary},
+    {<<"like_username">>, binary},
+    {<<"like_peerhost">>, binary},
+    {<<"like_peerhost_net">>, binary}
+]).
+
 namespace() ->
     undefined.
 
@@ -60,15 +72,16 @@ schema("/banned") ->
         get => #{
             description => ?DESC(list_banned_api),
             tags => ?TAGS,
-            parameters => [
-                hoconsc:ref(emqx_dashboard_swagger, page),
-                hoconsc:ref(emqx_dashboard_swagger, limit)
-            ],
+            parameters => fields(get_banned_parameters),
             responses => #{
                 200 => [
                     {data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})},
                     {meta, hoconsc:mk(hoconsc:ref(emqx_dashboard_swagger, meta), #{})}
-                ]
+                ],
+                400 => emqx_dashboard_swagger:error_codes(
+                    ['BAD_REQUEST'],
+                    ?DESC(create_banned_api_response400)
+                )
             }
         },
         post => #{
@@ -123,6 +136,56 @@ schema("/banned/:as/:who") ->
         }
     }.
 
+fields(get_banned_parameters) ->
+    [
+        hoconsc:ref(emqx_dashboard_swagger, page),
+        hoconsc:ref(emqx_dashboard_swagger, limit),
+        {clientid,
+            hoconsc:mk(binary(), #{
+                in => query,
+                required => false,
+                desc => ?DESC(filter_clientid)
+            })},
+        {username,
+            hoconsc:mk(binary(), #{
+                in => query,
+                required => false,
+                desc => ?DESC(filter_username)
+            })},
+        {peerhost,
+            hoconsc:mk(binary(), #{
+                in => query,
+                required => false,
+                desc => ?DESC(filter_peerhost),
+                example => <<"127.0.0.1">>
+            })},
+        {like_clientid,
+            hoconsc:mk(binary(), #{
+                in => query,
+                required => false,
+                desc => ?DESC(filter_like_clientid)
+            })},
+        {like_username,
+            hoconsc:mk(binary(), #{
+                in => query,
+                required => false,
+                desc => ?DESC(filter_like_username)
+            })},
+        {like_peerhost,
+            hoconsc:mk(binary(), #{
+                in => query,
+                required => false,
+                desc => ?DESC(filter_like_peerhost),
+                example => <<"127.0.0.1">>
+            })},
+        {like_peerhost_net,
+            hoconsc:mk(binary(), #{
+                in => query,
+                required => false,
+                desc => ?DESC(filter_like_peerhost_net),
+                example => <<"192.1.0.0/16">>
+            })}
+    ];
 fields(ban) ->
     [
         {as,
@@ -156,21 +219,25 @@ fields(ban) ->
                 example => <<"2021-10-25T21:48:47+08:00">>
             })},
         {until,
-            hoconsc:mk(emqx_utils_calendar:epoch_second(), #{
+            hoconsc:mk(hoconsc:union([infinity, emqx_utils_calendar:epoch_second()]), #{
                 desc => ?DESC(until),
                 required => false,
+                default => infinity,
                 example => <<"2021-10-25T21:53:47+08:00">>
             })}
     ].
 
 banned(get, #{query_string := Params}) ->
-    Response = emqx_mgmt_api:paginate(emqx_banned:tables(), Params, ?FORMAT_FUN),
-    {200, Response};
+    case list_banned(Params) of
+        {200, _Response} = Resp ->
+            Resp;
+        {error, Reason} ->
+            {400, 'BAD_REQUEST', format_error(Reason)}
+    end;
 banned(post, #{body := Body}) ->
     case emqx_banned:parse(Body) of
         {error, Reason} ->
-            ErrorReason = io_lib:format("~p", [Reason]),
-            {400, 'BAD_REQUEST', list_to_binary(ErrorReason)};
+            {400, 'BAD_REQUEST', format_error(Reason)};
         {ok, Ban} ->
             case emqx_banned:create(Ban) of
                 {ok, Banned} ->
@@ -195,5 +262,118 @@ delete_banned(delete, #{bindings := Params}) ->
             {204}
     end.
 
+list_banned(Params) ->
+    {_Cnt, {QS, FuzzyQS}} = emqx_mgmt_api:parse_qstring(Params, ?BANNED_QSCHEMA),
+    list_banned(QS, FuzzyQS, Params).
+
+%% Filters are logically mutually exclusive, two filters always get the zero set,
+%% unless they are of the same type and one is exact and the other is fuzzy,
+%% this case is meaningless, so only one filter is allowed in a query.
+list_banned([], [], Params) ->
+    Response = emqx_mgmt_api:paginate(emqx_banned:tables(), Params, ?FORMAT_FUN),
+    {200, Response};
+list_banned([{As, '=:=', Who}], [], Params) ->
+    maybe
+        Key = {As, _Who1} ?= emqx_banned:parse_who(#{<<"as">> => As, <<"who">> => Who}),
+        Result = emqx_banned:look_up(Key),
+        MetaIn = emqx_mgmt_api:parse_pager_params(Params),
+        {200, #{
+            meta => MetaIn#{hasnext => false, count => erlang:length(Result)},
+            data => lists:map(fun format/1, Result)
+        }}
+    end;
+list_banned([], [_Who], Params) ->
+    {200,
+        emqx_mgmt_api:node_query_with_tabs(
+            node(),
+            emqx_banned:tables(),
+            Params,
+            ?BANNED_QSCHEMA,
+            fun ?MODULE:qs2ms/2,
+            fun ?MODULE:format/1
+        )};
+list_banned(_QS, _FuzzyQS, _Params) ->
+    {error, <<"too_many_filters">>}.
+
+qs2ms(_Tab, {_QS, FuzzyQS}) ->
+    #{
+        match_spec => ms_from_qstring(FuzzyQS),
+        fuzzy_fun => fuzzy_filter_fun(FuzzyQS)
+    }.
+
+ms_from_qstring([{username, like, _UserName}]) ->
+    ets:fun2ms(fun(#banned{who = {Type, _}} = Banned) when
+        Type =:= username; Type =:= username_re
+    ->
+        Banned
+    end);
+ms_from_qstring([{clientid, like, _ClientId}]) ->
+    ets:fun2ms(fun(#banned{who = {Type, _}} = Banned) when
+        Type =:= clientid; Type =:= clientid_re
+    ->
+        Banned
+    end);
+ms_from_qstring([{peerhost, like, _Peerhost}]) ->
+    ets:fun2ms(fun(#banned{who = {peerhost, _}} = Banned) ->
+        Banned
+    end);
+ms_from_qstring([{peerhost_net, like, _PeerhostNet}]) ->
+    ets:fun2ms(fun(#banned{who = {peerhost_net, _}} = Banned) ->
+        Banned
+    end);
+ms_from_qstring(_) ->
+    [].
+
+%% Fuzzy username funcs
+fuzzy_filter_fun([]) ->
+    undefined;
+fuzzy_filter_fun(Fuzzy) ->
+    {fun ?MODULE:run_fuzzy_filter/2, [Fuzzy]}.
+
+run_fuzzy_filter(_, []) ->
+    true;
+run_fuzzy_filter(
+    #banned{who = {DataType, DataValue}},
+    [{TargetType, like, TargetValue}]
+) ->
+    Validator = get_fuzzy_validator(DataType, TargetType),
+    Validator(DataValue, TargetValue);
+run_fuzzy_filter(_, []) ->
+    false.
+
+get_fuzzy_validator(username, username) ->
+    fun binary_fuzzy_validator/2;
+get_fuzzy_validator(username_re, username) ->
+    fun binary_fuzzy_validator/2;
+get_fuzzy_validator(clientid, clientid) ->
+    fun binary_fuzzy_validator/2;
+get_fuzzy_validator(clientid_re, clientid) ->
+    fun binary_fuzzy_validator/2;
+get_fuzzy_validator(peerhost, peerhost) ->
+    fun(Peerhost, Target) ->
+        Peerhost1 = inet:ntoa(Peerhost),
+        binary_fuzzy_validator(Peerhost1, Target)
+    end;
+get_fuzzy_validator(peerhost_net, peerhost_net) ->
+    fun(CIDR, Target) ->
+        CIDRBinary = erlang:list_to_binary(esockd_cidr:to_string(CIDR)),
+        binary_fuzzy_validator(CIDRBinary, Target)
+    end;
+get_fuzzy_validator(_, _) ->
+    fun(_, _) ->
+        false
+    end.
+
+binary_fuzzy_validator({_RE, Data}, Target) ->
+    re:run(Data, Target) =/= nomatch;
+binary_fuzzy_validator(Data, Target) ->
+    re:run(Data, Target) =/= nomatch.
+
 format(Banned) ->
     emqx_banned:format(Banned).
+
+format_error(Error) when is_binary(Error) ->
+    Error;
+format_error(Reason) ->
+    ErrorReason = io_lib:format("~p", [Reason]),
+    erlang:iolist_to_binary(ErrorReason).

+ 109 - 5
apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl

@@ -21,8 +21,6 @@
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("common_test/include/ct.hrl").
 
--define(EXPIRATION_TIME, 31536000).
-
 all() ->
     emqx_common_test_helpers:all(?MODULE).
 
@@ -185,14 +183,13 @@ t_create(_Config) ->
         at => At
     },
     {ok, ClientIdBannedRes2} = create_banned(ClientIdBanned2),
-    Until2 = emqx_banned:to_rfc3339(Now + ?EXPIRATION_TIME),
     ?assertEqual(
         #{
             <<"as">> => As,
             <<"at">> => At,
             <<"by">> => By,
             <<"reason">> => Reason,
-            <<"until">> => Until2,
+            <<"until">> => <<"infinity">>,
             <<"who">> => ClientId2
         },
         ClientIdBannedRes2
@@ -311,9 +308,81 @@ t_clear(_Config) ->
     ),
     ok.
 
+t_list_with_filters(_) ->
+    setup_list_test_data(),
+
+    %% clientid
+    test_for_list("clientid=c1", [<<"c1">>]),
+    test_for_list("clientid=c3", []),
+
+    %% username
+    test_for_list("username=u1", [<<"u1">>]),
+    test_for_list("username=u3", []),
+
+    %% peerhost
+    test_for_list("peerhost=192.168.1.1", [<<"192.168.1.1">>]),
+    test_for_list("peerhost=192.168.1.3", []),
+
+    %% like clientid
+    test_for_list("like_clientid=c", [<<"c1">>, <<"c2">>, <<"c[0-9]">>]),
+    test_for_list("like_clientid=c3", []),
+
+    %% like username
+    test_for_list("like_username=u", [<<"u1">>, <<"u2">>, <<"u[0-9]">>]),
+    test_for_list("like_username=u3", []),
+
+    %% like peerhost
+    test_for_list("like_peerhost=192.168", [<<"192.168.1.1">>, <<"192.168.1.2">>]),
+    test_for_list("like_peerhost=192.168.1.3", []),
+
+    %% like peerhost_net
+    test_for_list("like_peerhost_net=192.168", [<<"192.168.0.0/16">>]),
+    test_for_list("like_peerhost_net=192.166", []),
+
+    %% list all
+    test_for_list([], [
+        <<"c1">>,
+        <<"c2">>,
+        <<"u1">>,
+        <<"u2">>,
+        <<"192.168.1.1">>,
+        <<"192.168.1.2">>,
+        <<"c[0-9]">>,
+        <<"u[0-9]">>,
+        <<"192.168.0.0/16">>
+    ]),
+
+    %% page query with table join
+    R1 = get_who_from_list("like_clientid=c&page=1&limit=1"),
+    ?assertEqual(1, erlang:length(R1)),
+
+    R2 = get_who_from_list("like_clientid=c&page=2&limit=1"),
+    ?assertEqual(1, erlang:length(R2)),
+
+    R3 = get_who_from_list("like_clientid=c&page=3&limit=1"),
+    ?assertEqual(1, erlang:length(R2)),
+
+    ?assertEqual(
+        lists:sort(R1 ++ R2 ++ R3),
+        lists:sort([<<"c1">>, <<"c2">>, <<"c[0-9]">>])
+    ),
+
+    emqx_banned:clear(),
+    ok.
+
 list_banned() ->
+    list_banned([]).
+
+list_banned(Params) ->
     Path = emqx_mgmt_api_test_util:api_path(["banned"]),
-    case emqx_mgmt_api_test_util:request_api(get, Path) of
+    case
+        emqx_mgmt_api_test_util:request_api(
+            get,
+            Path,
+            Params,
+            emqx_mgmt_api_test_util:auth_header_()
+        )
+    of
         {ok, Apps} -> {ok, emqx_utils_json:decode(Apps, [return_maps])};
         Error -> Error
     end.
@@ -336,3 +405,38 @@ clear_banned() ->
 
 to_rfc3339(Sec) ->
     list_to_binary(calendar:system_time_to_rfc3339(Sec)).
+
+setup_list_test_data() ->
+    emqx_banned:clear(),
+
+    Data = [
+        {clientid, <<"c1">>},
+        {clientid, <<"c2">>},
+        {username, <<"u1">>},
+        {username, <<"u2">>},
+        {peerhost, <<"192.168.1.1">>},
+        {peerhost, <<"192.168.1.2">>},
+        {clientid_re, <<"c[0-9]">>},
+        {username_re, <<"u[0-9]">>},
+        {peerhost_net, <<"192.168.0.0/16">>}
+    ],
+
+    lists:foreach(
+        fun({As, Who}) ->
+            {ok, Banned} = emqx_banned:parse(#{<<"as">> => As, <<"who">> => Who}),
+            emqx_banned:create(Banned)
+        end,
+        Data
+    ).
+
+test_for_list(Params, Expected) ->
+    Result = list_banned(Params),
+    ?assertMatch({ok, #{<<"data">> := _}}, Result),
+    {ok, #{<<"data">> := Data}} = Result,
+    ?assertEqual(lists:sort(Expected), lists:sort([Who || #{<<"who">> := Who} <- Data])).
+
+get_who_from_list(Params) ->
+    Result = list_banned(Params),
+    ?assertMatch({ok, #{<<"data">> := _}}, Result),
+    {ok, #{<<"data">> := Data}} = Result,
+    [Who || #{<<"who">> := Who} <- Data].

+ 12 - 0
changes/ce/feat-13792.en.md

@@ -0,0 +1,12 @@
+The `GET /banned` endpoint supports querying using filters in the query string.
+The available filters are:
+- clientid
+- username
+- peerhost
+- like_clientid
+- like_username
+- like_peerhost
+- like_peerhost_net
+
+## Breaking changes
+* The default expiration time for a banned item that is created without an `until` value is now `infinity` up from 1 year.

+ 23 - 1
rel/i18n/emqx_mgmt_api_banned.hocon

@@ -36,7 +36,8 @@ delete_banned_api_response404.desc:
 """The banned object was not found in the blacklist."""
 
 list_banned_api.desc:
-"""List all currently banned client IDs, usernames and IP addresses."""
+"""List all currently banned client IDs, usernames and IP addresses.
+Filters are supported. Since filters are mutually exclusive, only one filter is allowed in a query."""
 list_banned_api.label:
 """List all banned client IDs"""
 
@@ -63,4 +64,25 @@ clear_banned_api.desc:
 clear_banned_api.label:
 """Clear"""
 
+filter_clientid.desc:
+"""Query the banned objects with an exact client ID."""
+
+filter_username.desc:
+"""Query the banned objects with an exact username."""
+
+filter_peerhost.desc:
+"""Query the banned objects with an exact IP address."""
+
+filter_like_clientid.desc:
+"""Fuzzy query banned objects with a client ID."""
+
+filter_like_username.desc:
+"""Fuzzy query banned objects with an username."""
+
+filter_like_peerhost.desc:
+"""Fuzzy query banned objects with an IP address."""
+
+filter_like_peerhost_net.desc:
+"""Fuzzy query banned objects with a CIDR."""
+
 }