From 18f18fc1a164c5a8a728f3ceec4f40c84d83773a Mon Sep 17 00:00:00 2001 From: Ery Lee Date: Mon, 6 Apr 2015 11:52:35 +0800 Subject: [PATCH] acl --- apps/emqttd/src/emqttd_access_rule.erl | 14 +-- apps/emqttd/src/emqttd_acl.erl | 51 +++++---- apps/emqttd/src/emqttd_acl_internal.erl | 138 +++++++++++++----------- apps/emqttd/test/emqttd_acl_tests.erl | 53 ++++----- 4 files changed, 139 insertions(+), 117 deletions(-) diff --git a/apps/emqttd/src/emqttd_access_rule.erl b/apps/emqttd/src/emqttd_access_rule.erl index 1c6dfcdb6..59f17d01c 100644 --- a/apps/emqttd/src/emqttd_access_rule.erl +++ b/apps/emqttd/src/emqttd_access_rule.erl @@ -53,8 +53,8 @@ compile({A, all}) when (A =:= allow) orelse (A =:= deny) -> {A, all}; -compile({A, Who, PubSub, TopicFilters}) when (A =:= allow) orelse (A =:= deny) -> - {A, compile(who, Who), PubSub, [compile(topic, bin(Topic)) || Topic <- TopicFilters]}. +compile({A, Who, Access, TopicFilters}) when (A =:= allow) orelse (A =:= deny) -> + {A, compile(who, Who), Access, [compile(topic, bin(Topic)) || Topic <- TopicFilters]}. compile(who, all) -> all; @@ -72,12 +72,12 @@ compile(who, {user, Username}) -> compile(topic, Topic) -> Words = emqttd_topic:words(Topic), - case pattern(Words) of + case 'pattern?'(Words) of true -> {pattern, Words}; false -> Words end. -pattern(Words) -> +'pattern?'(Words) -> lists:member(<<"$u">>, Words) orelse lists:member(<<"$c">>, Words). @@ -92,13 +92,13 @@ bin(B) when is_binary(B) -> %% %% @end %%%----------------------------------------------------------------------------- --spec match(mqtt_user(), binary(), rule()) -> {matched, allow} | {matched, deny} | nomatch. +-spec match(mqtt_user(), topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch. match(_User, _Topic, {AllowDeny, all}) when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> {matched, AllowDeny}; match(User, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) - when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> + when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> case match_who(User, Who) andalso match_topics(User, Topic, TopicFilters) of - true -> {matched, AllowDeny}; + true -> {matched, AllowDeny}; false -> nomatch end. diff --git a/apps/emqttd/src/emqttd_acl.erl b/apps/emqttd/src/emqttd_acl.erl index a0c142351..8086f21fc 100644 --- a/apps/emqttd/src/emqttd_acl.erl +++ b/apps/emqttd/src/emqttd_acl.erl @@ -35,7 +35,7 @@ -define(SERVER, ?MODULE). %% API Function Exports --export([start_link/1, check/3, reload/0, register_mod/1, unregister_mod/1]). +-export([start_link/1, check/3, reload/0, register_mod/1, unregister_mod/1, all_modules/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -49,9 +49,9 @@ -ifdef(use_specs). --callback check_acl(PubSub, User, Topic) -> {ok, allow | deny} | ignore | {error, any()} when - PubSub :: publish | subscribe, +-callback check_acl(User, PubSub, Topic) -> {ok, allow | deny} | ignore | {error, any()} when User :: mqtt_user(), + PubSub :: publish | subscribe, Topic :: binary(). -callback reload_acl() -> ok | {error, any()}. @@ -88,23 +88,23 @@ start_link(AclOpts) -> %% %% @end %%------------------------------------------------------------------------------ --spec check(PubSub, User, Topic) -> {ok, allow | deny} | {error, any()} when - PubSub :: publish | subscribe, +-spec check(User, PubSub, Topic) -> {ok, allow | deny} | {error, any()} when User :: mqtt_user(), + PubSub :: publish | subscribe, Topic :: binary(). -check(PubSub, User, Topic) when PubSub =:= publish orelse PubSub =:= subscribe -> - case ets:lookup(?ACL_TABLE, acl_mods) of +check(User, PubSub, Topic) when PubSub =:= publish orelse PubSub =:= subscribe -> + case ets:lookup(?ACL_TABLE, acl_modules) of [] -> {error, "No ACL mods!"}; - [{_, Mods}] -> check(PubSub, User, Topic, Mods) + [{_, Mods}] -> check(User, PubSub, Topic, Mods) end. -check(_PubSub, _User, _Topic, []) -> +check(_User, _PubSub, _Topic, []) -> {error, "All ACL mods ignored!"}; -check(PubSub, User, Topic, [Mod|Mods]) -> - case Mod:check_acl(PubSub, User, Topic) of +check(User, PubSub, Topic, [Mod|Mods]) -> + case Mod:check_acl(User, PubSub, Topic) of {ok, AllowDeny} -> {ok, AllowDeny}; - ignore -> check(PubSub, User, Topic, Mods) + ignore -> check(User, PubSub, Topic, Mods) end. %%------------------------------------------------------------------------------ @@ -114,7 +114,7 @@ check(PubSub, User, Topic, [Mod|Mods]) -> %% @end %%------------------------------------------------------------------------------ reload() -> - case ets:lookup(?ACL_TABLE, acl_mods) of + case ets:lookup(?ACL_TABLE, acl_modules) of [] -> {error, "No ACL mod!"}; [{_, Mods}] -> [M:reload() || M <- Mods] end. @@ -139,6 +139,18 @@ register_mod(Mod) -> unregister_mod(Mod) -> gen_server:call(?SERVER, {unregister_mod, Mod}). +%%------------------------------------------------------------------------------ +%% @doc +%% All ACL Modules. +%% +%% @end +%%------------------------------------------------------------------------------ +all_modules() -> + case ets:lookup(?ACL_TABLE, acl_modules) of + [] -> []; + [{_, Mods}] -> Mods + end. + %%%============================================================================= %%% gen_server callbacks. %%%============================================================================= @@ -147,20 +159,20 @@ init([_AclOpts]) -> {ok, state}. handle_call({register_mod, Mod}, _From, State) -> - Mods = acl_mods(), + Mods = all_modules(), case lists:member(Mod, Mods) of true -> {reply, {error, registered}, State}; false -> - ets:insert(?ACL_TABLE, {acl_mods, [Mod | Mods]}), + ets:insert(?ACL_TABLE, {acl_modules, [Mod | Mods]}), {reply, ok, State} end; handle_call({unregister_mod, Mod}, _From, State) -> - Mods = acl_mods(), + Mods = all_modules(), case lists:member(Mod, Mods) of true -> - ets:insert(?ACL_TABLE, lists:delete(Mod, Mods)), + ets:insert(?ACL_TABLE, {acl_modules, lists:delete(Mod, Mods)}), {reply, ok, State}; false -> {reply, {error, not_found}, State} @@ -185,9 +197,4 @@ code_change(_OldVsn, State, _Extra) -> %%%============================================================================= %%% Internal functions %%%============================================================================= -acl_mods() -> - case ets:lookup(?ACL_TABLE, acl_mods) of - [] -> []; - [{_, Mods}] -> Mods - end. diff --git a/apps/emqttd/src/emqttd_acl_internal.erl b/apps/emqttd/src/emqttd_acl_internal.erl index 722f61a6e..cada39b1a 100644 --- a/apps/emqttd/src/emqttd_acl_internal.erl +++ b/apps/emqttd/src/emqttd_acl_internal.erl @@ -30,14 +30,16 @@ -include("emqttd.hrl"). --define(SERVER, ?MODULE). +-export([start_link/1]). + +-behaviour(emqttd_acl). + +%% ACL callbacks +-export([check_acl/3, reload_acl/0, description/0]). -behaviour(gen_server). --export([start_link/1]). - -%% acl callbacks --export([check_acl/3, reload_acl/0]). +-define(SERVER, ?MODULE). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -58,27 +60,33 @@ %% @end %%------------------------------------------------------------------------------ -spec start_link(AclOpts) -> {ok, pid()} | ignore | {error, any()} when - AclOpts :: [{file, list()}]. + AclOpts :: [{file, list()}]. start_link(AclOpts) -> gen_server:start_link({local, ?SERVER}, ?MODULE, [AclOpts], []). --spec check_acl(PubSub, User, Topic) -> {ok, allow} | {ok, deny} | ignore | {error, any()} when - PubSub :: publish | subscribe, +%%%============================================================================= +%%% ACL callbacks +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% Check ACL. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec check_acl(User, PubSub, Topic) -> {ok, allow} | {ok, deny} | ignore | {error, any()} when User :: mqtt_user(), + PubSub :: publish | subscribe, Topic :: binary(). -check_acl(PubSub, User, Topic) -> +check_acl(User, PubSub, Topic) -> case match(User, Topic, lookup(PubSub)) of {matched, allow} -> {ok, allow}; {matched, deny} -> {ok, deny}; nomatch -> {error, nomatch} end. --spec reload_acl() -> ok. -reload_acl() -> - gen_server:call(?SERVER, reload). - lookup(PubSub) -> - case ets:lookup(emqttd_acl:table(), PubSub) of + case ets:lookup(?ACL_RULE_TABLE, PubSub) of [] -> []; [{PubSub, Rules}] -> Rules end. @@ -87,41 +95,40 @@ match(_User, _Topic, []) -> nomatch; match(User, Topic, [Rule|Rules]) -> - case emqttd_acl_rule:match(User, Topic, Rule) of + case emqttd_access_rule:match(User, Topic, Rule) of nomatch -> match(User, Topic, Rules); {matched, AllowDeny} -> {matched, AllowDeny} end. -%% ------------------------------------------------------------------ -%% gen_server Function Definitions -%% ------------------------------------------------------------------ +%%------------------------------------------------------------------------------ +%% @doc +%% Reload ACL. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec reload_acl() -> ok. +reload_acl() -> + gen_server:call(?SERVER, reload). + +%%------------------------------------------------------------------------------ +%% @doc +%% ACL Description. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec description() -> string(). +description() -> + "Internal ACL with etc/acl.config". + +%%%============================================================================= +%%% gen_server callbacks +%%%============================================================================= + init([AclOpts]) -> - ets:insert(emqttd_acl:table(), {acl_mods, [?MODULE]}), + ets:new(?ACL_RULE_TABLE, [set, proteted, named_table]), AclFile = proplists:get_value(file, AclOpts), load_rules(#state{acl_file = AclFile}). -load_rules(State = #state{acl_file = AclFile}) -> - {ok, Terms} = file:consult(AclFile), - Rules = [compile(Term) || Term <- Terms], - lists:foreach(fun(PubSub) -> - ets:insert(?ACL_TAB, {PubSub, - lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) - end, [publish, subscribe]), - {ok, State#state{raw_rules = Terms}}. - -filter(_PubSub, {allow, all}) -> - true; -filter(_PubSub, {deny, all}) -> - true; -filter(publish, {_AllowDeny, _Who, publish, _Topics}) -> - true; -filter(_PubSub, {_AllowDeny, _Who, pubsub, _Topics}) -> - true; -filter(subscribe, {_AllowDeny, _Who, subscribe, _Topics}) -> - true; -filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> - false. - handle_call(reload, _From, State) -> case catch load_rules(State) of {ok, NewState} -> @@ -130,28 +137,8 @@ handle_call(reload, _From, State) -> {reply, {error, Error}, State} end; -handle_call({register_mod, Mod}, _From, State) -> - [{_, Mods}] = ets:lookup(?ACL_TAB, acl_mods), - case lists:member(Mod, Mods) of - true -> - {reply, {error, registered}, State}; - false -> - ets:insert(?ACL_TAB, {acl_mods, [Mod|Mods]}), - {reply, ok, State} - end; - -handle_call({unregister_mod, Mod}, _From, State) -> - [{_, Mods}] = ets:lookup(?ACL_TAB, acl_mods), - case lists:member(Mod, Mods) of - true -> - ets:insert(?ACL_TAB, lists:delete(Mod, Mods)), - {reply, ok, State}; - false -> - {reply, {error, not_found}, State} - end; - handle_call(Req, _From, State) -> - lager:error("Bad Request: ~p", [Req]), + lager:error("BadReq: ~p", [Req]), {reply, {error, badreq}, State}. handle_cast(_Msg, State) -> @@ -163,3 +150,30 @@ handle_info(_Info, State) -> terminate(_Reason, _State) -> ok. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +load_rules(State = #state{acl_file = AclFile}) -> + {ok, Terms} = file:consult(AclFile), + Rules = [emqttd_access_rule:compile(Term) || Term <- Terms], + lists:foreach(fun(PubSub) -> + ets:insert(?ACL_RULE_TABLE, {PubSub, + lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) + end, [publish, subscribe]), + {ok, State#state{raw_rules = Terms}}. + +filter(_PubSub, {allow, all}) -> + true; +filter(publish, {_AllowDeny, _Who, publish, _Topics}) -> + true; +filter(_PubSub, {_AllowDeny, _Who, pubsub, _Topics}) -> + true; +filter(subscribe, {_AllowDeny, _Who, subscribe, _Topics}) -> + true; +filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> + false. + diff --git a/apps/emqttd/test/emqttd_acl_tests.erl b/apps/emqttd/test/emqttd_acl_tests.erl index 22619878c..8ad832119 100644 --- a/apps/emqttd/test/emqttd_acl_tests.erl +++ b/apps/emqttd/test/emqttd_acl_tests.erl @@ -26,6 +26,8 @@ %%%----------------------------------------------------------------------------- -module(emqttd_acl_tests). +-import(emqttd_access_rule, [compile/1, match/3]). + -include("emqttd.hrl"). -ifdef(TEST). @@ -34,44 +36,43 @@ compile_test() -> ?assertMatch({allow, {ipaddr, {"127.0.0.1", _I, _I}}, subscribe, [ [<<"$SYS">>, '#'], ['#'] ]}, - emqttd_acl:compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]})), + compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]})), ?assertMatch({allow, {user, <<"testuser">>}, subscribe, [ [<<"a">>, <<"b">>, <<"c">>], [<<"d">>, <<"e">>, <<"f">>, '#'] ]}, - emqttd_acl:compile({allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]})), + compile({allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]})), ?assertEqual({allow, {user, <<"admin">>}, pubsub, [ [<<"d">>, <<"e">>, <<"f">>, '#'] ]}, - emqttd_acl:compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]})), + compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]})), ?assertEqual({allow, {client, <<"testClient">>}, publish, [ [<<"testTopics">>, <<"testClient">>] ]}, - emqttd_acl:compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]})), + compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]})), ?assertEqual({allow, all, pubsub, [{pattern, [<<"clients">>, <<"$c">>]}]}, - emqttd_acl:compile({allow, all, pubsub, ["clients/$c"]})), + compile({allow, all, pubsub, ["clients/$c"]})), ?assertEqual({allow, all, subscribe, [{pattern, [<<"users">>, <<"$u">>, '#']}]}, - emqttd_acl:compile({allow, all, subscribe, ["users/$u/#"]})), + compile({allow, all, subscribe, ["users/$u/#"]})), ?assertEqual({deny, all, subscribe, [ [<<"$SYS">>, '#'], ['#'] ]}, - emqttd_acl:compile({deny, all, subscribe, ["$SYS/#", "#"]})), - ?assertEqual({allow, all}, emqttd_acl:compile({allow, all})), - ?assertEqual({deny, all}, emqttd_acl:compile({deny, all})). + compile({deny, all, subscribe, ["$SYS/#", "#"]})), + ?assertEqual({allow, all}, compile({allow, all})), + ?assertEqual({deny, all}, compile({deny, all})). match_test() -> User = #mqtt_user{ipaddr = {127,0,0,1}, clientid = <<"testClient">>, username = <<"TestUser">>}, User2 = #mqtt_user{ipaddr = {192,168,0,10}, clientid = <<"testClient">>, username = <<"TestUser">>}, - ?assertEqual({matched, allow}, emqttd_acl:match(User, <<"Test/Topic">>, [{allow, all}])), - ?assertEqual({matched, deny}, emqttd_acl:match(User, <<"Test/Topic">>, [{deny, all}])), - ?assertMatch({matched, allow}, emqttd_acl:match(User, <<"Test/Topic">>, - emqttd_acl:compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}))), - ?assertMatch({matched, allow}, emqttd_acl:match(User2, <<"Test/Topic">>, - emqttd_acl:compile({allow, {ipaddr, "192.168.0.1/24"}, subscribe, ["$SYS/#", "#"]}))), - ?assertMatch({matched, allow}, emqttd_acl:match(User, <<"d/e/f/x">>, - emqttd_acl:compile({allow, {user, "TestUser"}, subscribe, ["a/b/c", "d/e/f/#"]}))), - ?assertEqual(nomatch, emqttd_acl:match(User, <<"d/e/f/x">>, emqttd_access:compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]}))), - ?assertMatch({matched, allow}, emqttd_acl:match(User, <<"testTopics/testClient">>, - emqttd_acl:compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]}))), - ?assertMatch({matched, allow}, emqttd_acl:match(User, <<"clients/testClient">>, - emqttd_acl:compile({allow, all, pubsub, ["clients/$c"]}))), - ?assertMatch({matched, allow}, emqttd_acl:match(#mqtt_user{username = <<"user2">>}, <<"users/user2/abc/def">>, - emqttd_acl:compile({allow, all, subscribe, ["users/$u/#"]}))), + ?assertEqual({matched, allow}, match(User, <<"Test/Topic">>, {allow, all})), + ?assertEqual({matched, deny}, match(User, <<"Test/Topic">>, {deny, all})), + ?assertMatch({matched, allow}, match(User, <<"Test/Topic">>, + compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}))), + ?assertMatch({matched, allow}, match(User2, <<"Test/Topic">>, + compile({allow, {ipaddr, "192.168.0.1/24"}, subscribe, ["$SYS/#", "#"]}))), + ?assertMatch({matched, allow}, match(User, <<"d/e/f/x">>, compile({allow, {user, "TestUser"}, subscribe, ["a/b/c", "d/e/f/#"]}))), + ?assertEqual(nomatch, match(User, <<"d/e/f/x">>, compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]}))), + ?assertMatch({matched, allow}, match(User, <<"testTopics/testClient">>, + compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]}))), + ?assertMatch({matched, allow}, match(User, <<"clients/testClient">>, + compile({allow, all, pubsub, ["clients/$c"]}))), + ?assertMatch({matched, allow}, match(#mqtt_user{username = <<"user2">>}, <<"users/user2/abc/def">>, + compile({allow, all, subscribe, ["users/$u/#"]}))), ?assertMatch({matched, deny}, - emqttd_acl:match(User, <<"d/e/f">>, - emqttd_acl:compile({deny, all, subscribe, ["$SYS/#", "#"]}))). + match(User, <<"d/e/f">>, + compile({deny, all, subscribe, ["$SYS/#", "#"]}))). -endif.