218 lines
6.6 KiB
Erlang
218 lines
6.6 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2020-2023 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_authz_rule).
|
|
|
|
-include("emqx_authz.hrl").
|
|
-include_lib("emqx/include/logger.hrl").
|
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
|
|
|
-ifdef(TEST).
|
|
-compile(export_all).
|
|
-compile(nowarn_export_all).
|
|
-endif.
|
|
|
|
%% APIs
|
|
-export([
|
|
match/4,
|
|
matches/4,
|
|
compile/1
|
|
]).
|
|
|
|
-type ipaddress() ::
|
|
{ipaddr, esockd_cidr:cidr_string()}
|
|
| {ipaddrs, list(esockd_cidr:cidr_string())}.
|
|
|
|
-type username() :: {username, binary()}.
|
|
|
|
-type clientid() :: {clientid, binary()}.
|
|
|
|
-type who() ::
|
|
ipaddress()
|
|
| username()
|
|
| clientid()
|
|
| {'and', [ipaddress() | username() | clientid()]}
|
|
| {'or', [ipaddress() | username() | clientid()]}
|
|
| all.
|
|
|
|
-type action() :: subscribe | publish | all.
|
|
-type permission() :: allow | deny.
|
|
|
|
-type rule() :: {permission(), who(), action(), list(emqx_types:topic())}.
|
|
|
|
-export_type([
|
|
action/0,
|
|
permission/0
|
|
]).
|
|
|
|
compile({Permission, all}) when
|
|
?ALLOW_DENY(Permission)
|
|
->
|
|
{Permission, all, all, [compile_topic(<<"#">>)]};
|
|
compile({Permission, Who, Action, TopicFilters}) when
|
|
?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters)
|
|
->
|
|
{atom(Permission), compile_who(Who), atom(Action), [
|
|
compile_topic(Topic)
|
|
|| Topic <- TopicFilters
|
|
]}.
|
|
|
|
compile_who(all) ->
|
|
all;
|
|
compile_who({user, Username}) ->
|
|
compile_who({username, Username});
|
|
compile_who({username, {re, Username}}) ->
|
|
{ok, MP} = re:compile(bin(Username)),
|
|
{username, MP};
|
|
compile_who({username, Username}) ->
|
|
{username, {eq, bin(Username)}};
|
|
compile_who({client, Clientid}) ->
|
|
compile_who({clientid, Clientid});
|
|
compile_who({clientid, {re, Clientid}}) ->
|
|
{ok, MP} = re:compile(bin(Clientid)),
|
|
{clientid, MP};
|
|
compile_who({clientid, Clientid}) ->
|
|
{clientid, {eq, bin(Clientid)}};
|
|
compile_who({ipaddr, CIDR}) ->
|
|
{ipaddr, esockd_cidr:parse(CIDR, true)};
|
|
compile_who({ipaddrs, CIDRs}) ->
|
|
{ipaddrs, lists:map(fun(CIDR) -> esockd_cidr:parse(CIDR, true) end, CIDRs)};
|
|
compile_who({'and', L}) when is_list(L) ->
|
|
{'and', [compile_who(Who) || Who <- L]};
|
|
compile_who({'or', L}) when is_list(L) ->
|
|
{'or', [compile_who(Who) || Who <- L]}.
|
|
|
|
compile_topic(<<"eq ", Topic/binary>>) ->
|
|
{eq, emqx_topic:words(Topic)};
|
|
compile_topic({eq, Topic}) ->
|
|
{eq, emqx_topic:words(bin(Topic))};
|
|
compile_topic(Topic) ->
|
|
TopicBin = bin(Topic),
|
|
case
|
|
emqx_placeholder:preproc_tmpl(
|
|
TopicBin,
|
|
#{placeholders => [?PH_USERNAME, ?PH_CLIENTID]}
|
|
)
|
|
of
|
|
[{str, _}] -> emqx_topic:words(TopicBin);
|
|
Tokens -> {pattern, Tokens}
|
|
end.
|
|
|
|
atom(B) when is_binary(B) ->
|
|
try
|
|
binary_to_existing_atom(B, utf8)
|
|
catch
|
|
_E:_S -> binary_to_atom(B)
|
|
end;
|
|
atom(A) when is_atom(A) -> A.
|
|
|
|
bin(L) when is_list(L) ->
|
|
list_to_binary(L);
|
|
bin(B) when is_binary(B) ->
|
|
B.
|
|
|
|
-spec matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()]) ->
|
|
{matched, allow} | {matched, deny} | nomatch.
|
|
matches(_Client, _PubSub, _Topic, []) ->
|
|
nomatch;
|
|
matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) ->
|
|
case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of
|
|
nomatch -> matches(Client, PubSub, Topic, Tail);
|
|
Matched -> Matched
|
|
end.
|
|
|
|
-spec match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule()) ->
|
|
{matched, allow} | {matched, deny} | nomatch.
|
|
match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) ->
|
|
case
|
|
match_action(PubSub, Action) andalso
|
|
match_who(Client, Who) andalso
|
|
match_topics(Client, Topic, TopicFilters)
|
|
of
|
|
true -> {matched, Permission};
|
|
_ -> nomatch
|
|
end.
|
|
|
|
match_action(publish, publish) -> true;
|
|
match_action(subscribe, subscribe) -> true;
|
|
match_action(_, all) -> true;
|
|
match_action(_, _) -> false.
|
|
|
|
match_who(_, all) ->
|
|
true;
|
|
match_who(#{username := undefined}, {username, _}) ->
|
|
false;
|
|
match_who(#{username := Username}, {username, {eq, Username}}) ->
|
|
true;
|
|
match_who(#{username := Username}, {username, {re_pattern, _, _, _, _} = MP}) ->
|
|
case re:run(Username, MP) of
|
|
{match, _} -> true;
|
|
_ -> false
|
|
end;
|
|
match_who(#{clientid := Clientid}, {clientid, {eq, Clientid}}) ->
|
|
true;
|
|
match_who(#{clientid := Clientid}, {clientid, {re_pattern, _, _, _, _} = MP}) ->
|
|
case re:run(Clientid, MP) of
|
|
{match, _} -> true;
|
|
_ -> false
|
|
end;
|
|
match_who(#{peerhost := undefined}, {ipaddr, _CIDR}) ->
|
|
false;
|
|
match_who(#{peerhost := IpAddress}, {ipaddr, CIDR}) ->
|
|
esockd_cidr:match(IpAddress, CIDR);
|
|
match_who(#{peerhost := undefined}, {ipaddrs, _CIDR}) ->
|
|
false;
|
|
match_who(#{peerhost := IpAddress}, {ipaddrs, CIDRs}) ->
|
|
lists:any(
|
|
fun(CIDR) ->
|
|
esockd_cidr:match(IpAddress, CIDR)
|
|
end,
|
|
CIDRs
|
|
);
|
|
match_who(ClientInfo, {'and', Principals}) when is_list(Principals) ->
|
|
lists:foldl(
|
|
fun(Principal, Permission) ->
|
|
match_who(ClientInfo, Principal) andalso Permission
|
|
end,
|
|
true,
|
|
Principals
|
|
);
|
|
match_who(ClientInfo, {'or', Principals}) when is_list(Principals) ->
|
|
lists:foldl(
|
|
fun(Principal, Permission) ->
|
|
match_who(ClientInfo, Principal) orelse Permission
|
|
end,
|
|
false,
|
|
Principals
|
|
);
|
|
match_who(_, _) ->
|
|
false.
|
|
|
|
match_topics(_ClientInfo, _Topic, []) ->
|
|
false;
|
|
match_topics(ClientInfo, Topic, [{pattern, PatternFilter} | Filters]) ->
|
|
TopicFilter = emqx_placeholder:proc_tmpl(PatternFilter, ClientInfo),
|
|
match_topic(emqx_topic:words(Topic), emqx_topic:words(TopicFilter)) orelse
|
|
match_topics(ClientInfo, Topic, Filters);
|
|
match_topics(ClientInfo, Topic, [TopicFilter | Filters]) ->
|
|
match_topic(emqx_topic:words(Topic), TopicFilter) orelse
|
|
match_topics(ClientInfo, Topic, Filters).
|
|
|
|
match_topic(Topic, {'eq', TopicFilter}) ->
|
|
Topic =:= TopicFilter;
|
|
match_topic(Topic, TopicFilter) ->
|
|
emqx_topic:match(Topic, TopicFilter).
|