diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 58fdc5f98..633527b57 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -126,8 +126,7 @@ -record(banned, { who :: {clientid, binary()} | {peerhost, inet:ip_address()} - | {username, binary()} - | {ip_address, inet:ip_address()}, + | {username, binary()}, by :: binary(), reason :: binary(), at :: integer(), diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index c143a20a6..715548d41 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/apps/emqx/src/emqx_banned.erl @@ -33,8 +33,10 @@ -export([ check/1 , create/1 + , look_up/1 , delete/1 , info/1 + , format/1 ]). %% gen_server callbacks @@ -90,7 +92,31 @@ do_check(Who) when is_tuple(Who) -> Until > erlang:system_time(second) end. --spec(create(emqx_types:banned()) -> ok). +format(#banned{who = Who0, + by = By, + reason = Reason, + at = At, + until = Until}) -> + {As, Who} = maybe_format_host(Who0), + #{ + as => As, + who => Who, + by => By, + reason => Reason, + at => to_rfc3339(At), + until => to_rfc3339(Until) + }. + +maybe_format_host({peerhost, Host}) -> + AddrBinary = list_to_binary(inet:ntoa(Host)), + {peerhost, AddrBinary}; +maybe_format_host({As, Who}) -> + {As, Who}. + +to_rfc3339(Timestamp) -> + list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, second}])). + +-spec(create(emqx_types:banned() | map()) -> ok). create(#{who := Who, by := By, reason := Reason, @@ -104,6 +130,9 @@ create(#{who := Who, create(Banned) when is_record(Banned, banned) -> ekka_mnesia:dirty_write(?BANNED_TAB, Banned). +look_up(Who) -> + mnesia:dirty_read(?BANNED_TAB, Who). + -spec(delete({clientid, emqx_types:clientid()} | {username, emqx_types:username()} | {peerhost, emqx_types:peerhost()}) -> ok). diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl new file mode 100644 index 000000000..18abbd7e1 --- /dev/null +++ b/apps/emqx_management/src/emqx_mgmt_api_banned.erl @@ -0,0 +1,144 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_mgmt_api_banned). + +-include_lib("emqx/include/emqx.hrl"). + +-include("emqx_mgmt.hrl"). + +-behaviour(minirest_api). + +-export([api_spec/0]). + +-export([ banned/2 + , delete_banned/2 + ]). + +-import(emqx_mgmt_util, [ page_params/0 + , schema/1 + , object_schema/1 + , page_object_schema/1 + , properties/1 + , error_schema/1 + ]). + +-export([format/1]). + +-define(TAB, emqx_banned). + +api_spec() -> + {[banned_api(), delete_banned_api()], []}. + +-define(BANNED_TYPES, [clientid, username, peerhost]). + +properties() -> + properties([ + {as, string, <<"Banned type clientid, username, peerhost">>, [clientid, username, peerhost]}, + {who, string, <<"Client info as banned type">>}, + {by, integer, <<"Commander">>}, + {reason, string, <<"Banned reason">>}, + {at, integer, <<"Create banned time. Nullable, rfc3339, default is now">>}, + {until, string, <<"Cancel banned time. Nullable, rfc3339, default is now + 5 minute">>} + ]). + +banned_api() -> + Path = "/banned", + MetaData = #{ + get => #{ + description => <<"List banned">>, + parameters => page_params(), + responses => #{ + <<"200">> => + page_object_schema(properties())}}, + post => #{ + description => <<"Create banned">>, + 'requestBody' => object_schema(properties()), + responses => #{ + <<"200">> => schema(<<"Create success">>)}}}, + {Path, MetaData, banned}. + +delete_banned_api() -> + Path = "/banned/:as/:who", + MetaData = #{ + delete => #{ + description => <<"Delete banned">>, + parameters => [ + #{ + name => as, + in => path, + required => true, + description => <<"Banned type">>, + schema => #{type => string, enum => ?BANNED_TYPES} + }, + #{ + name => who, + in => path, + required => true, + description => <<"Client info as banned type">>, + schema => #{type => string} + } + ], + responses => #{ + <<"200">> => schema(<<"Delete banned success">>), + <<"404">> => error_schema(<<"Banned not found">>)}}}, + {Path, MetaData, delete_banned}. + +banned(get, #{query_string := Params}) -> + Response = emqx_mgmt_api:paginate(?TAB, Params, fun format/1), + {200, Response}; +banned(post, #{body := Body}) -> + Banned = trans_param(Body), + _ = emqx_banned:create(Banned), + {200}. + +delete_banned(delete, #{bindings := Params}) -> + Who = trans_who(Params), + case emqx_banned:look_up(Who) of + [] -> + As0 = maps:get(as, Params), + Who0 = maps:get(who, Params), + Message = list_to_binary(io_lib:format("~p: ~p not found", [As0, Who0])), + {404, #{code => 'RESOURCE_NOT_FOUND', message => Message}}; + _ -> + ok = emqx_banned:delete(Who), + {200} + end. + +trans_param(Params) -> + Who = trans_who(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 + 5 * 60), + #banned{ + who = Who, + by = By, + reason = Reason, + at = At, + until = Until + }. + +trans_who(#{as := As, who := Who}) -> + trans_who(#{<<"as">> => As, <<"who">> => Who}); +trans_who(#{<<"as">> := <<"peerhost">>, <<"who">> := Peerhost0}) -> + {ok, Peerhost} = inet:parse_address(binary_to_list(Peerhost0)), + {peerhost, Peerhost}; +trans_who(#{<<"as">> := As, <<"who">> := Who}) -> + {binary_to_atom(As, utf8), Who}. + +format(Banned) -> + emqx_banned:format(Banned).