Преглед изворни кода

feat(authz): support authorization config file part 1.

zhanghongtong пре 4 година
родитељ
комит
a7fac1a7a3

+ 30 - 0
apps/emqx_authz/etc/authorization_rules.conf

@@ -0,0 +1,30 @@
+%%--------------------------------------------------------------------
+%% -type(ipaddress() :: {ipaddress, string() | [string()]})
+%%
+%% -type(username() :: {username, regex()})
+%%
+%% -type(clientid() :: {clientid, regex()})
+%%
+%% -type(who() :: ipaddress() | username() | clientid() |
+%%                {'and', [ipaddress() | username() | clientid()]} |
+%%                {'or',  [ipaddress() | username() | clientid()]} |
+%%                all).
+%%
+%% -type(action() :: subscribe | publish | all).
+%%
+%% -type(topic_filters() :: string()).
+%%
+%% -type(topics() :: [topic_filters() | {eq, topic_filters()}]).
+%%
+%% -type(permission() :: allow | deny).
+%%
+%% -type(rule() :: {permission(), who(), access(), topics()}).
+%%--------------------------------------------------------------------
+
+{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}.
+
+{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}.
+
+{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
+
+{allow, all}.

+ 4 - 0
apps/emqx_authz/etc/emqx_authz.conf

@@ -1,5 +1,9 @@
 authorization_rules {
     rules = [
+       # {
+       #      type: file
+       #      path: {{ platform_etc_dir }}/authorization_rules.conf
+       # },
        # {
        #      type: http
        #      config: {

+ 17 - 1
apps/emqx_authz/include/emqx_authz.hrl

@@ -1,4 +1,20 @@
--type(rule() :: #{atom() => any()}).
+-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_topic:topic())}).
 -type(rules() :: [rule()]).
 
 -define(APP, emqx_authz).

+ 22 - 0
apps/emqx_authz/src/emqx_authz.erl

@@ -253,6 +253,28 @@ init_rule(#{topics := Topics,
            } = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) ->
     init_rule(Rule#{annotations =>#{id => gen_id(simple)}});
 
+init_rule(#{principal := Principal,
+            enable := true,
+            type := file,
+            path := Path
+           } = Rule) ->
+    Rules = case file:consult(Path) of
+                {ok, Terms} ->
+                    [emqx_authz_rule:compile(Term) || Term <- Terms];
+                {error, eacces} ->
+                    ?LOG(alert, "Insufficient permissions to read the ~s file", [Path]),
+                    error(eaccess);
+                {error, enoent} ->
+                    ?LOG(alert, "The ~s file does not exist", [Path]),
+                    error(enoent);
+                {error, Reason} ->
+                    ?LOG(alert, "Failed to read ~s: ~p", [Path, Reason]),
+                    error(Reason)
+            end,
+    Rule#{annotations =>
+            #{id => gen_id(file),
+              rules => Rules
+         }};
 init_rule(#{principal := Principal,
             enable := true,
             type := http,

+ 148 - 0
apps/emqx_authz/src/emqx_authz_rule.erl

@@ -0,0 +1,148 @@
+%%--------------------------------------------------------------------
+%% 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_authz_rule).
+
+-include("emqx_authz.hrl").
+-include_lib("emqx/include/logger.hrl").
+
+-ifdef(TEST).
+-compile(export_all).
+-compile(nowarn_export_all).
+-endif.
+
+%% APIs
+-export([ match/4
+        , compile/1
+        ]).
+
+-export_type([rule/0]).
+
+compile({Permission, Who, Action, TopicFilters}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) ->
+    {Permission, compile_who(Who), Action, [compile_topic(Topic) || Topic <- TopicFilters]};
+compile({Permission, Who, Action, Topic}) when ?ALLOW_DENY(Permission), ?PUBSUB(Action) ->
+    {Permission, compile_who(Who), Action, [compile_topic(Topic)]}.
+
+compile_who(all) -> all;
+compile_who({username, Username}) ->
+    {ok, MP} = re:compile(bin(Username)),
+    {username, MP};
+compile_who({clientid, Clientid}) ->
+    {ok, MP} = re:compile(bin(Clientid)),
+    {clientid, MP};
+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}) ->
+    {eq, emqx_topic:words(bin(Topic))};
+compile_topic(Topic) ->
+    Words = emqx_topic:words(bin(Topic)),
+    case pattern(Words) of
+        true  -> {pattern, Words};
+        false -> Words
+    end.
+
+pattern(Words) ->
+    lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words).
+
+bin(L) when is_list(L) ->
+    list_to_binary(L);
+bin(B) when is_binary(B) ->
+    B.
+
+-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, _MP}) ->
+    false;
+match_who(#{username := Username}, {username, MP}) ->
+    case re:run(Username, MP) of
+        {match, _} -> true;
+        _ -> false
+    end;
+match_who(#{clientid := Clientid}, {clientid, 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 = feed_var(ClientInfo, PatternFilter),
+    match_topic(emqx_topic:words(Topic), 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).
+
+feed_var(ClientInfo, Pattern) ->
+    feed_var(ClientInfo, Pattern, []).
+feed_var(_ClientInfo, [], Acc) ->
+    lists:reverse(Acc);
+feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) ->
+    feed_var(ClientInfo, Words, [<<"%c">>|Acc]);
+feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) ->
+    feed_var(ClientInfo, Words, [ClientId |Acc]);
+feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) ->
+    feed_var(ClientInfo, Words, [<<"%u">>|Acc]);
+feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) ->
+    feed_var(ClientInfo, Words, [Username|Acc]);
+feed_var(ClientInfo, [W|Words], Acc) ->
+    feed_var(ClientInfo, Words, [W|Acc]).

+ 14 - 0
apps/emqx_authz/src/emqx_authz_schema.erl

@@ -22,6 +22,19 @@ structs() -> ["authorization_rules"].
 fields("authorization_rules") ->
     [ {rules, rules()}
     ];
+fields(file) ->
+    [ {principal, principal()}
+    , {type, #{type => http}}
+    , {enable, #{type => boolean(),
+                 default => true}}
+    , {path, #{type => string(),
+               validator => fun(S) -> case filelib:is_file(S) of
+                                        true -> ok;
+                                        _ -> {error, "File does not exist"}
+                                      end
+                            end
+              }}
+    ];
 fields(http) ->
     [ {principal, principal()}
     , {type, #{type => http}}
@@ -148,6 +161,7 @@ union_array(Item) when is_list(Item) ->
 rules() ->
     #{type => union_array(
                 [ hoconsc:ref(?MODULE, simple_rule)
+                , hoconsc:ref(?MODULE, file)
                 , hoconsc:ref(?MODULE, http)
                 , hoconsc:ref(?MODULE, mysql)
                 , hoconsc:ref(?MODULE, pgsql)

+ 138 - 0
apps/emqx_authz/test/emqx_authz_rule_SUITE.erl

@@ -0,0 +1,138 @@
+%%--------------------------------------------------------------------
+%% 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_authz_rule_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-include("emqx_authz.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+
+-define(RULE1, {deny,  all, all, ["#"]}).
+-define(RULE2, {allow, {ipaddr,  "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}).
+-define(RULE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, ["%c"]}).
+-define(RULE4, {allow, {'and', [{clientid, "^test?"}, {username, "^test?"}]}, publish, ["topic/test"]}).
+-define(RULE5, {allow, {'or',  [{username, "^test"},  {clientid, "test?"}]},  publish, ["%u", "%c"]}).
+
+all() ->
+    emqx_ct:all(?MODULE).
+
+init_per_suite(Config) ->
+    ok = emqx_ct_helpers:start_apps([emqx_authz]),
+    Config.
+
+end_per_suite(_Config) ->
+    emqx_ct_helpers:stop_apps([emqx_authz]),
+    ok.
+
+t_compile(_) ->
+    ?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?RULE1)),
+
+    ?assertEqual({allow, {ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]}, emqx_authz_rule:compile(?RULE2)),
+
+    ?assertEqual({allow,
+                  {ipaddrs,[{{127,0,0,1},{127,0,0,1},32},
+                            {{192,168,1,0},{192,168,1,255},24}]},
+                  subscribe,
+                  [{pattern,[<<"%c">>]}]
+               }, emqx_authz_rule:compile(?RULE3)),
+
+    ?assertMatch({allow,
+                  {'and', [{clientid, {re_pattern, _, _, _, _}}, {username, {re_pattern, _, _, _, _}}]},
+                  publish,
+                  [[<<"topic">>, <<"test">>]]
+                 }, emqx_authz_rule:compile(?RULE4)),
+
+    ?assertMatch({allow,
+                  {'or', [{username, {re_pattern, _, _, _, _}}, {clientid, {re_pattern, _, _, _, _}}]},
+                  publish,
+                  [{pattern, [<<"%u">>]},  {pattern, [<<"%c">>]}]
+                 }, emqx_authz_rule:compile(?RULE5)),
+    ok.
+
+
+t_match(_) ->
+    ClientInfo1 = #{clientid => <<"test">>,
+                    username => <<"test">>,
+                    peerhost => {127,0,0,1},
+                    zone => default,
+                    listener => mqtt_tcp
+                   },
+    ClientInfo2 = #{clientid => <<"test">>,
+                    username => <<"test">>,
+                    peerhost => {192,168,1,10},
+                    zone => default,
+                    listener => mqtt_tcp
+                   },
+    ClientInfo3 = #{clientid => <<"test">>,
+                    username => <<"fake">>,
+                    peerhost => {127,0,0,1},
+                    zone => default,
+                    listener => mqtt_tcp
+                   },
+    ClientInfo4 = #{clientid => <<"fake">>,
+                    username => <<"test">>,
+                    peerhost => {127,0,0,1},
+                    zone => default,
+                    listener => mqtt_tcp
+                   },
+
+    ?assertEqual({matched, deny},
+                emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE1))),
+    ?assertEqual({matched, deny},
+                emqx_authz_rule:match(ClientInfo2, subscribe, <<"+">>, emqx_authz_rule:compile(?RULE1))),
+    ?assertEqual({matched, deny},
+                emqx_authz_rule:match(ClientInfo3, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE1))),
+
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo1, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE2))),
+    ?assertEqual(nomatch,
+                emqx_authz_rule:match(ClientInfo1, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE2))),
+    ?assertEqual(nomatch,
+                emqx_authz_rule:match(ClientInfo2, subscribe, <<"#">>, emqx_authz_rule:compile(?RULE2))),
+
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo1, subscribe, <<"test">>, emqx_authz_rule:compile(?RULE3))),
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo2, subscribe, <<"test">>, emqx_authz_rule:compile(?RULE3))),
+    ?assertEqual(nomatch,
+                emqx_authz_rule:match(ClientInfo2, subscribe, <<"topic/test">>, emqx_authz_rule:compile(?RULE3))),
+
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo1, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))),
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo2, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))),
+    ?assertEqual(nomatch,
+                emqx_authz_rule:match(ClientInfo3, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))),
+    ?assertEqual(nomatch,
+                emqx_authz_rule:match(ClientInfo4, publish, <<"topic/test">>, emqx_authz_rule:compile(?RULE4))),
+
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo1, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))),
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo2, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))),
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo3, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))),
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo3, publish, <<"fake">>, emqx_authz_rule:compile(?RULE5))),
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo4, publish, <<"test">>, emqx_authz_rule:compile(?RULE5))),
+    ?assertEqual({matched, allow},
+                emqx_authz_rule:match(ClientInfo4, publish, <<"fake">>, emqx_authz_rule:compile(?RULE5))),
+
+    ok.
+

+ 1 - 0
rebar.config.erl

@@ -340,6 +340,7 @@ relx_overlay(ReleaseType) ->
     , {copy, "bin/emqx_ctl", "bin/emqx_ctl-{{release_version}}"} %% for relup
     , {copy, "bin/install_upgrade.escript", "bin/install_upgrade.escript-{{release_version}}"} %% for relup
     , {copy, "apps/emqx_gateway/src/lwm2m/lwm2m_xml", "etc/lwm2m_xml"}
+    , {copy, "apps/emqx_authz/etc/authorization_rules.conf", "etc/authorization_rules.conf"}
     , {template, "bin/emqx.cmd", "bin/emqx.cmd"}
     , {template, "bin/emqx_ctl.cmd", "bin/emqx_ctl.cmd"}
     , {copy, "bin/nodetool", "bin/nodetool"}