feat(authz): support authorization config file part 1.
This commit is contained in:
parent
1886aa8bff
commit
a7fac1a7a3
|
@ -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}.
|
|
@ -1,5 +1,9 @@
|
|||
authorization_rules {
|
||||
rules = [
|
||||
# {
|
||||
# type: file
|
||||
# path: {{ platform_etc_dir }}/authorization_rules.conf
|
||||
# },
|
||||
# {
|
||||
# type: http
|
||||
# config: {
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]).
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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"}
|
||||
|
|
Loading…
Reference in New Issue