feat(authz): use extensible map format for actions in authz rules

* support authorization on retain, qos fields
* refactored authz tests heavily
This commit is contained in:
Ilya Averyanov 2023-06-01 23:19:19 +03:00
parent d2bea433f5
commit 7de26a1776
51 changed files with 3144 additions and 1693 deletions

View File

@ -18,3 +18,17 @@
-define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME, "authorization"). -define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME, "authorization").
-define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_ATOM, authorization). -define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_ATOM, authorization).
-define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_BINARY, <<"authorization">>). -define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_BINARY, <<"authorization">>).
-define(DEFAULT_ACTION_QOS, 0).
-define(DEFAULT_ACTION_RETAIN, false).
-define(AUTHZ_SUBSCRIBE(QOS), #{action_type => subscribe, qos => QOS}).
-define(AUTHZ_SUBSCRIBE, ?AUTHZ_SUBSCRIBE(?DEFAULT_ACTION_QOS)).
-define(AUTHZ_PUBLISH(QOS, RETAIN), #{action_type => publish, qos => QOS, retain => RETAIN}).
-define(AUTHZ_PUBLISH(QOS), ?AUTHZ_PUBLISH(QOS, ?DEFAULT_ACTION_RETAIN)).
-define(AUTHZ_PUBLISH, ?AUTHZ_PUBLISH(?DEFAULT_ACTION_QOS)).
-define(authz_action(PUBSUB, QOS), #{action_type := PUBSUB, qos := QOS}).
-define(authz_action(PUBSUB), ?authz_action(PUBSUB, _)).
-define(authz_action, ?authz_action(_)).

View File

@ -21,7 +21,7 @@
-define(PH(Type), <<"${", Type/binary, "}">>). -define(PH(Type), <<"${", Type/binary, "}">>).
%% action: publish/subscribe/all %% action: publish/subscribe
-define(PH_ACTION, <<"${action}">>). -define(PH_ACTION, <<"${action}">>).
%% cert %% cert
@ -79,6 +79,7 @@
-define(PH_REASON, <<"${reason}">>). -define(PH_REASON, <<"${reason}">>).
-define(PH_ENDPOINT_NAME, <<"${endpoint_name}">>). -define(PH_ENDPOINT_NAME, <<"${endpoint_name}">>).
-define(PH_RETAIN, <<"${retain}">>).
%% sync change these place holder with binary def. %% sync change these place holder with binary def.
-define(PH_S_ACTION, "${action}"). -define(PH_S_ACTION, "${action}").
@ -113,5 +114,6 @@
-define(PH_S_NODE, "${node}"). -define(PH_S_NODE, "${node}").
-define(PH_S_REASON, "${reason}"). -define(PH_S_REASON, "${reason}").
-define(PH_S_ENDPOINT_NAME, "${endpoint_name}"). -define(PH_S_ENDPOINT_NAME, "${endpoint_name}").
-define(PH_S_RETAIN, "${retain}").
-endif. -endif.

View File

@ -77,10 +77,10 @@ authenticate(Credential) ->
%% @doc Check Authorization %% @doc Check Authorization
-spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -> -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) ->
allow | deny. allow | deny.
authorize(ClientInfo, PubSub, <<"$delayed/", Data/binary>> = RawTopic) -> authorize(ClientInfo, Action, <<"$delayed/", Data/binary>> = RawTopic) ->
case binary:split(Data, <<"/">>) of case binary:split(Data, <<"/">>) of
[_, Topic] -> [_, Topic] ->
authorize(ClientInfo, PubSub, Topic); authorize(ClientInfo, Action, Topic);
_ -> _ ->
?SLOG(warning, #{ ?SLOG(warning, #{
msg => "invalid_delayed_topic_format", msg => "invalid_delayed_topic_format",
@ -90,39 +90,39 @@ authorize(ClientInfo, PubSub, <<"$delayed/", Data/binary>> = RawTopic) ->
inc_authz_metrics(deny), inc_authz_metrics(deny),
deny deny
end; end;
authorize(ClientInfo, PubSub, Topic) -> authorize(ClientInfo, Action, Topic) ->
Result = Result =
case emqx_authz_cache:is_enabled() of case emqx_authz_cache:is_enabled() of
true -> check_authorization_cache(ClientInfo, PubSub, Topic); true -> check_authorization_cache(ClientInfo, Action, Topic);
false -> do_authorize(ClientInfo, PubSub, Topic) false -> do_authorize(ClientInfo, Action, Topic)
end, end,
inc_authz_metrics(Result), inc_authz_metrics(Result),
Result. Result.
check_authorization_cache(ClientInfo, PubSub, Topic) -> check_authorization_cache(ClientInfo, Action, Topic) ->
case emqx_authz_cache:get_authz_cache(PubSub, Topic) of case emqx_authz_cache:get_authz_cache(Action, Topic) of
not_found -> not_found ->
AuthzResult = do_authorize(ClientInfo, PubSub, Topic), AuthzResult = do_authorize(ClientInfo, Action, Topic),
emqx_authz_cache:put_authz_cache(PubSub, Topic, AuthzResult), emqx_authz_cache:put_authz_cache(Action, Topic, AuthzResult),
AuthzResult; AuthzResult;
AuthzResult -> AuthzResult ->
emqx:run_hook( emqx:run_hook(
'client.check_authz_complete', 'client.check_authz_complete',
[ClientInfo, PubSub, Topic, AuthzResult, cache] [ClientInfo, Action, Topic, AuthzResult, cache]
), ),
inc_authz_metrics(cache_hit), inc_authz_metrics(cache_hit),
AuthzResult AuthzResult
end. end.
do_authorize(ClientInfo, PubSub, Topic) -> do_authorize(ClientInfo, Action, Topic) ->
NoMatch = emqx:get_config([authorization, no_match], allow), NoMatch = emqx:get_config([authorization, no_match], allow),
Default = #{result => NoMatch, from => default}, Default = #{result => NoMatch, from => default},
case run_hooks('client.authorize', [ClientInfo, PubSub, Topic], Default) of case run_hooks('client.authorize', [ClientInfo, Action, Topic], Default) of
AuthzResult = #{result := Result} when Result == allow; Result == deny -> AuthzResult = #{result := Result} when Result == allow; Result == deny ->
From = maps:get(from, AuthzResult, unknown), From = maps:get(from, AuthzResult, unknown),
emqx:run_hook( emqx:run_hook(
'client.check_authz_complete', 'client.check_authz_complete',
[ClientInfo, PubSub, Topic, Result, From] [ClientInfo, Action, Topic, Result, From]
), ),
Result; Result;
Other -> Other ->
@ -133,7 +133,7 @@ do_authorize(ClientInfo, PubSub, Topic) ->
}), }),
emqx:run_hook( emqx:run_hook(
'client.check_authz_complete', 'client.check_authz_complete',
[ClientInfo, PubSub, Topic, deny, unknown_return_format] [ClientInfo, Action, Topic, deny, unknown_return_format]
), ),
deny deny
end. end.

View File

@ -16,7 +16,7 @@
-module(emqx_authz_cache). -module(emqx_authz_cache).
-include("emqx.hrl"). -include("emqx_access_control.hrl").
-export([ -export([
list_authz_cache/0, list_authz_cache/0,
@ -159,8 +159,7 @@ dump_authz_cache() ->
map_authz_cache(Fun) -> map_authz_cache(Fun) ->
[ [
Fun(R) Fun(R)
|| R = {{SubPub, _T}, _Authz} <- erlang:get(), || R = {{?authz_action, _T}, _Authz} <- erlang:get()
SubPub =:= publish orelse SubPub =:= subscribe
]. ].
foreach_authz_cache(Fun) -> foreach_authz_cache(Fun) ->
_ = map_authz_cache(Fun), _ = map_authz_cache(Fun),

View File

@ -20,6 +20,7 @@
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_channel.hrl"). -include("emqx_channel.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include("emqx_access_control.hrl").
-include("logger.hrl"). -include("logger.hrl").
-include("types.hrl"). -include("types.hrl").
@ -491,7 +492,7 @@ handle_in(
ok -> ok ->
TopicFilters0 = parse_topic_filters(TopicFilters), TopicFilters0 = parse_topic_filters(TopicFilters),
TopicFilters1 = enrich_subopts_subid(Properties, TopicFilters0), TopicFilters1 = enrich_subopts_subid(Properties, TopicFilters0),
TupleTopicFilters0 = check_sub_authzs(TopicFilters1, Channel), TupleTopicFilters0 = check_sub_authzs(SubPkt, TopicFilters1, Channel),
HasAuthzDeny = lists:any( HasAuthzDeny = lists:any(
fun({_TopicFilter, ReasonCode}) -> fun({_TopicFilter, ReasonCode}) ->
ReasonCode =:= ?RC_NOT_AUTHORIZED ReasonCode =:= ?RC_NOT_AUTHORIZED
@ -1838,14 +1839,34 @@ check_pub_alias(
check_pub_alias(_Packet, _Channel) -> check_pub_alias(_Packet, _Channel) ->
ok. ok.
%%--------------------------------------------------------------------
%% Athorization action
authz_action(#mqtt_packet{
header = #mqtt_packet_header{qos = QoS, retain = Retain}, variable = #mqtt_packet_publish{}
}) ->
?AUTHZ_PUBLISH(QoS, Retain);
authz_action(#mqtt_packet{
header = #mqtt_packet_header{qos = QoS}, variable = #mqtt_packet_subscribe{}
}) ->
?AUTHZ_SUBSCRIBE(QoS);
%% Will message
authz_action(#message{qos = QoS, flags = #{retain := Retain}}) ->
?AUTHZ_PUBLISH(QoS, Retain);
authz_action(#message{qos = QoS}) ->
?AUTHZ_PUBLISH(QoS).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Check Pub Authorization %% Check Pub Authorization
check_pub_authz( check_pub_authz(
#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, #mqtt_packet{
variable = #mqtt_packet_publish{topic_name = Topic}
} = Packet,
#channel{clientinfo = ClientInfo} #channel{clientinfo = ClientInfo}
) -> ) ->
case emqx_access_control:authorize(ClientInfo, publish, Topic) of Action = authz_action(Packet),
case emqx_access_control:authorize(ClientInfo, Action, Topic) of
allow -> ok; allow -> ok;
deny -> {error, ?RC_NOT_AUTHORIZED} deny -> {error, ?RC_NOT_AUTHORIZED}
end. end.
@ -1868,24 +1889,23 @@ check_pub_caps(
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Check Sub Authorization %% Check Sub Authorization
%% TODO: not only check topic filter. Qos chould be checked too. check_sub_authzs(Packet, TopicFilters, Channel) ->
%% Not implemented yet: Action = authz_action(Packet),
%% MQTT-3.1.1 [MQTT-3.8.4-6] and MQTT-5.0 [MQTT-3.8.4-7] check_sub_authzs(Action, TopicFilters, Channel, []).
check_sub_authzs(TopicFilters, Channel) ->
check_sub_authzs(TopicFilters, Channel, []).
check_sub_authzs( check_sub_authzs(
Action,
[TopicFilter = {Topic, _} | More], [TopicFilter = {Topic, _} | More],
Channel = #channel{clientinfo = ClientInfo}, Channel = #channel{clientinfo = ClientInfo},
Acc Acc
) -> ) ->
case emqx_access_control:authorize(ClientInfo, subscribe, Topic) of case emqx_access_control:authorize(ClientInfo, Action, Topic) of
allow -> allow ->
check_sub_authzs(More, Channel, [{TopicFilter, ?RC_SUCCESS} | Acc]); check_sub_authzs(Action, More, Channel, [{TopicFilter, ?RC_SUCCESS} | Acc]);
deny -> deny ->
check_sub_authzs(More, Channel, [{TopicFilter, ?RC_NOT_AUTHORIZED} | Acc]) check_sub_authzs(Action, More, Channel, [{TopicFilter, ?RC_NOT_AUTHORIZED} | Acc])
end; end;
check_sub_authzs([], _Channel, Acc) -> check_sub_authzs(_Action, [], _Channel, Acc) ->
lists:reverse(Acc). lists:reverse(Acc).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -2149,7 +2169,8 @@ publish_will_msg(
ClientInfo = #{mountpoint := MountPoint}, ClientInfo = #{mountpoint := MountPoint},
Msg = #message{topic = Topic} Msg = #message{topic = Topic}
) -> ) ->
PublishingDisallowed = emqx_access_control:authorize(ClientInfo, publish, Topic) =/= allow, Action = authz_action(Msg),
PublishingDisallowed = emqx_access_control:authorize(ClientInfo, Action, Topic) =/= allow,
ClientBanned = emqx_banned:check(ClientInfo), ClientBanned = emqx_banned:check(ClientInfo),
case PublishingDisallowed orelse ClientBanned of case PublishingDisallowed orelse ClientBanned of
true -> true ->

View File

@ -29,6 +29,7 @@
-export_type([ -export_type([
zone/0, zone/0,
pubsub/0, pubsub/0,
pubsub_action/0,
subid/0 subid/0
]). ]).
@ -127,7 +128,12 @@
| exactly_once. | exactly_once.
-type zone() :: atom(). -type zone() :: atom().
-type pubsub() :: publish | subscribe. -type pubsub_action() :: publish | subscribe.
-type pubsub() ::
#{action_type := subscribe, qos := qos()}
| #{action_type := publish, qos := qos(), retain := boolean()}.
-type subid() :: binary() | atom(). -type subid() :: binary() | atom().
-type group() :: binary() | undefined. -type group() :: binary() | undefined.

View File

@ -19,8 +19,8 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/emqx_hooks.hrl"). -include_lib("emqx/include/emqx_hooks.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
all() -> emqx_common_test_helpers:all(?MODULE). all() -> emqx_common_test_helpers:all(?MODULE).
@ -44,8 +44,7 @@ t_authenticate(_) ->
?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())).
t_authorize(_) -> t_authorize(_) ->
Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), ?assertEqual(allow, emqx_access_control:authorize(clientinfo(), ?AUTHZ_PUBLISH, <<"t">>)).
?assertEqual(allow, emqx_access_control:authorize(clientinfo(), Publish, <<"t">>)).
t_delayed_authorize(_) -> t_delayed_authorize(_) ->
RawTopic = <<"$delayed/1/foo/2">>, RawTopic = <<"$delayed/1/foo/2">>,
@ -54,11 +53,11 @@ t_delayed_authorize(_) ->
ok = emqx_hooks:put('client.authorize', {?MODULE, authz_stub, [Topic]}, ?HP_AUTHZ), ok = emqx_hooks:put('client.authorize', {?MODULE, authz_stub, [Topic]}, ?HP_AUTHZ),
Publish1 = ?PUBLISH_PACKET(?QOS_0, RawTopic, 1, <<"payload">>), ?assertEqual(allow, emqx_access_control:authorize(clientinfo(), ?AUTHZ_PUBLISH, RawTopic)),
?assertEqual(allow, emqx_access_control:authorize(clientinfo(), Publish1, RawTopic)),
Publish2 = ?PUBLISH_PACKET(?QOS_0, InvalidTopic, 1, <<"payload">>), ?assertEqual(
?assertEqual(deny, emqx_access_control:authorize(clientinfo(), Publish2, InvalidTopic)), deny, emqx_access_control:authorize(clientinfo(), ?AUTHZ_PUBLISH, InvalidTopic)
),
ok. ok.
t_quick_deny_anonymous(_) -> t_quick_deny_anonymous(_) ->
@ -96,8 +95,8 @@ t_quick_deny_anonymous(_) ->
%% Helper functions %% Helper functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
authz_stub(_Client, _PubSub, ValidTopic, _DefaultResult, ValidTopic) -> {stop, #{result => allow}}; authz_stub(_Client, _Action, ValidTopic, _DefaultResult, ValidTopic) -> {stop, #{result => allow}};
authz_stub(_Client, _PubSub, _Topic, _DefaultResult, _ValidTopic) -> {stop, #{result => deny}}. authz_stub(_Client, _Action, _Topic, _DefaultResult, _ValidTopic) -> {stop, #{result => deny}}.
quick_deny_anonymous_authn(#{username := <<"badname">>}, _AuthResult) -> quick_deny_anonymous_authn(#{username := <<"badname">>}, _AuthResult) ->
{stop, {error, not_authorized}}; {stop, {error, not_authorized}};

View File

@ -43,8 +43,6 @@ t_clean_authz_cache(_) ->
ct:sleep(100), ct:sleep(100),
ClientPid = ClientPid =
case emqx_cm:lookup_channels(<<"emqx_c">>) of case emqx_cm:lookup_channels(<<"emqx_c">>) of
[Pid] when is_pid(Pid) ->
Pid;
Pids when is_list(Pids) -> Pids when is_list(Pids) ->
lists:last(Pids); lists:last(Pids);
_ -> _ ->

View File

@ -908,7 +908,8 @@ t_check_pub_alias(_) ->
t_check_sub_authzs(_) -> t_check_sub_authzs(_) ->
emqx_config:put_zone_conf(default, [authorization, enable], true), emqx_config:put_zone_conf(default, [authorization, enable], true),
TopicFilter = {<<"t">>, ?DEFAULT_SUBOPTS}, TopicFilter = {<<"t">>, ?DEFAULT_SUBOPTS},
[{TopicFilter, 0}] = emqx_channel:check_sub_authzs([TopicFilter], channel()). Subscribe = ?SUBSCRIBE_PACKET(1, [TopicFilter]),
[{TopicFilter, 0}] = emqx_channel:check_sub_authzs(Subscribe, [TopicFilter], channel()).
t_enrich_connack_caps(_) -> t_enrich_connack_caps(_) ->
ok = meck:new(emqx_mqtt_caps, [passthrough, no_history]), ok = meck:new(emqx_mqtt_caps, [passthrough, no_history]),

View File

@ -20,6 +20,7 @@
-include_lib("proper/include/proper.hrl"). -include_lib("proper/include/proper.hrl").
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_access_control.hrl").
%% High level Types %% High level Types
-export([ -export([
@ -34,7 +35,8 @@
subopts/0, subopts/0,
nodename/0, nodename/0,
normal_topic/0, normal_topic/0,
normal_topic_filter/0 normal_topic_filter/0,
pubsub/0
]). ]).
%% Basic Types %% Basic Types
@ -482,6 +484,23 @@ normal_topic_filter() ->
end end
). ).
subscribe_action() ->
?LET(
Qos,
qos(),
?AUTHZ_SUBSCRIBE(Qos)
).
publish_action() ->
?LET(
{Qos, Retain},
{qos(), boolean()},
?AUTHZ_PUBLISH(Qos, Retain)
).
pubsub() ->
oneof([publish_action(), subscribe_action()]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Basic Types %% Basic Types
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -18,16 +18,6 @@
-define(APP, emqx_authz). -define(APP, emqx_authz).
-define(ALLOW_DENY(A),
((A =:= allow) orelse (A =:= <<"allow">>) orelse
(A =:= deny) orelse (A =:= <<"deny">>))
).
-define(PUBSUB(A),
((A =:= subscribe) orelse (A =:= <<"subscribe">>) orelse
(A =:= publish) orelse (A =:= <<"publish">>) orelse
(A =:= all) orelse (A =:= <<"all">>))
).
%% authz_mnesia %% authz_mnesia
-define(ACL_TABLE, emqx_acl). -define(ACL_TABLE, emqx_acl).
@ -72,6 +62,20 @@
topic => <<"eq test/#">>, topic => <<"eq test/#">>,
permission => <<"deny">>, permission => <<"deny">>,
action => <<"all">> action => <<"all">>
},
#{
topic => <<"test/toopic/3">>,
permission => <<"allow">>,
action => <<"publish">>,
qos => [<<"1">>],
retain => <<"true">>
},
#{
topic => <<"test/toopic/4">>,
permission => <<"allow">>,
action => <<"publish">>,
qos => [<<"0">>, <<"1">>, <<"2">>],
retain => <<"all">>
} }
] ]
}). }).
@ -92,6 +96,20 @@
topic => <<"eq test/#">>, topic => <<"eq test/#">>,
permission => <<"deny">>, permission => <<"deny">>,
action => <<"all">> action => <<"all">>
},
#{
topic => <<"test/toopic/3">>,
permission => <<"allow">>,
action => <<"publish">>,
qos => [<<"1">>],
retain => <<"true">>
},
#{
topic => <<"test/toopic/4">>,
permission => <<"allow">>,
action => <<"publish">>,
qos => [<<"0">>, <<"1">>, <<"2">>],
retain => <<"all">>
} }
] ]
}). }).
@ -111,9 +129,28 @@
topic => <<"eq test/#">>, topic => <<"eq test/#">>,
permission => <<"deny">>, permission => <<"deny">>,
action => <<"all">> action => <<"all">>
},
#{
topic => <<"test/toopic/3">>,
permission => <<"allow">>,
action => <<"publish">>,
qos => [<<"1">>],
retain => <<"true">>
},
#{
topic => <<"test/toopic/4">>,
permission => <<"allow">>,
action => <<"publish">>,
qos => [<<"0">>, <<"1">>, <<"2">>],
retain => <<"all">>
} }
] ]
}). }).
-define(USERNAME_RULES_EXAMPLE_COUNT, length(maps:get(rules, ?USERNAME_RULES_EXAMPLE))).
-define(CLIENTID_RULES_EXAMPLE_COUNT, length(maps:get(rules, ?CLIENTID_RULES_EXAMPLE))).
-define(ALL_RULES_EXAMPLE_COUNT, length(maps:get(rules, ?ALL_RULES_EXAMPLE))).
-define(META_EXAMPLE, #{ -define(META_EXAMPLE, #{
page => 1, page => 1,
limit => 100, limit => 100,
@ -121,3 +158,8 @@
}). }).
-define(RESOURCE_GROUP, <<"emqx_authz">>). -define(RESOURCE_GROUP, <<"emqx_authz">>).
-define(AUTHZ_FEATURES, [rich_actions]).
-define(DEFAULT_RULE_QOS, [0, 1, 2]).
-define(DEFAULT_RULE_RETAIN, all).

View File

@ -39,6 +39,11 @@
get_enabled_authzs/0 get_enabled_authzs/0
]). ]).
-export([
feature_available/1,
set_feature_available/2
]).
-export([post_config_update/5, pre_config_update/3]). -export([post_config_update/5, pre_config_update/3]).
-export([acl_conf_file/0]). -export([acl_conf_file/0]).
@ -519,6 +524,28 @@ read_acl_file(#{<<"path">> := Path} = Source) ->
{ok, Rules} = emqx_authz_file:read_file(Path), {ok, Rules} = emqx_authz_file:read_file(Path),
maps:remove(<<"path">>, Source#{<<"rules">> => Rules}). maps:remove(<<"path">>, Source#{<<"rules">> => Rules}).
%%------------------------------------------------------------------------------
%% Extednded Features
%%------------------------------------------------------------------------------
-if(?EMQX_RELEASE_EDITION == ee).
-define(DEFAULT_RICH_ACTIONS, true).
-else.
-define(DEFAULT_RICH_ACTIONS, false).
-endif.
-define(FEATURE_KEY(_NAME_), {?MODULE, _NAME_}).
feature_available(rich_actions) ->
persistent_term:get(?FEATURE_KEY(rich_actions), ?DEFAULT_RICH_ACTIONS).
set_feature_available(Feature, Enable) when is_boolean(Enable) ->
persistent_term:put(?FEATURE_KEY(Feature), Enable).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Internal function %% Internal function
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -359,6 +359,22 @@ fields(rule_item) ->
required => true, required => true,
example => publish example => publish
} }
)},
{qos,
mk(
array(emqx_schema:qos()),
#{
desc => ?DESC(qos),
default => ?DEFAULT_RULE_QOS
}
)},
{retain,
mk(
hoconsc:union([all, boolean()]),
#{
desc => ?DESC(retain),
default => ?DEFAULT_RULE_RETAIN
}
)} )}
]; ];
fields(clientid) -> fields(clientid) ->
@ -434,7 +450,7 @@ users(post, #{body := Body}) when is_list(Body) ->
[] -> [] ->
lists:foreach( lists:foreach(
fun(#{<<"username">> := Username, <<"rules">> := Rules}) -> fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)) emqx_authz_mnesia:store_rules({username, Username}, Rules)
end, end,
Body Body
), ),
@ -470,7 +486,7 @@ clients(post, #{body := Body}) when is_list(Body) ->
[] -> [] ->
lists:foreach( lists:foreach(
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) -> fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules)) emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules)
end, end,
Body Body
), ),
@ -489,21 +505,14 @@ user(get, #{bindings := #{username := Username}}) ->
{ok, Rules} -> {ok, Rules} ->
{200, #{ {200, #{
username => Username, username => Username,
rules => [ rules => format_rules(Rules)
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}} }}
end; end;
user(put, #{ user(put, #{
bindings := #{username := Username}, bindings := #{username := Username},
body := #{<<"username">> := Username, <<"rules">> := Rules} body := #{<<"username">> := Username, <<"rules">> := Rules}
}) -> }) ->
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)), emqx_authz_mnesia:store_rules({username, Username}, Rules),
{204}; {204};
user(delete, #{bindings := #{username := Username}}) -> user(delete, #{bindings := #{username := Username}}) ->
case emqx_authz_mnesia:get_rules({username, Username}) of case emqx_authz_mnesia:get_rules({username, Username}) of
@ -521,21 +530,14 @@ client(get, #{bindings := #{clientid := ClientID}}) ->
{ok, Rules} -> {ok, Rules} ->
{200, #{ {200, #{
clientid => ClientID, clientid => ClientID,
rules => [ rules => format_rules(Rules)
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}} }}
end; end;
client(put, #{ client(put, #{
bindings := #{clientid := ClientID}, bindings := #{clientid := ClientID},
body := #{<<"clientid">> := ClientID, <<"rules">> := Rules} body := #{<<"clientid">> := ClientID, <<"rules">> := Rules}
}) -> }) ->
emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules)), emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules),
{204}; {204};
client(delete, #{bindings := #{clientid := ClientID}}) -> client(delete, #{bindings := #{clientid := ClientID}}) ->
case emqx_authz_mnesia:get_rules({clientid, ClientID}) of case emqx_authz_mnesia:get_rules({clientid, ClientID}) of
@ -552,18 +554,11 @@ all(get, _) ->
{200, #{rules => []}}; {200, #{rules => []}};
{ok, Rules} -> {ok, Rules} ->
{200, #{ {200, #{
rules => [ rules => format_rules(Rules)
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}} }}
end; end;
all(post, #{body := #{<<"rules">> := Rules}}) -> all(post, #{body := #{<<"rules">> := Rules}}) ->
emqx_authz_mnesia:store_rules(all, format_rules(Rules)), emqx_authz_mnesia:store_rules(all, Rules),
{204}; {204};
all(delete, _) -> all(delete, _) ->
emqx_authz_mnesia:store_rules(all, []), emqx_authz_mnesia:store_rules(all, []),
@ -626,58 +621,20 @@ run_fuzzy_filter(
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% format funcs %% format funcs
%% format rule from api
format_rules(Rules) when is_list(Rules) ->
lists:foldl(
fun(
#{
<<"topic">> := Topic,
<<"action">> := Action,
<<"permission">> := Permission
},
AccIn
) when
?PUBSUB(Action) andalso
?ALLOW_DENY(Permission)
->
AccIn ++ [{atom(Permission), atom(Action), Topic}]
end,
[],
Rules
).
%% format result from mnesia tab %% format result from mnesia tab
format_result([{username, Username}, {rules, Rules}]) -> format_result([{username, Username}, {rules, Rules}]) ->
#{ #{
username => Username, username => Username,
rules => [ rules => format_rules(Rules)
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}; };
format_result([{clientid, ClientID}, {rules, Rules}]) -> format_result([{clientid, ClientID}, {rules, Rules}]) ->
#{ #{
clientid => ClientID, clientid => ClientID,
rules => [ rules => format_rules(Rules)
#{
topic => Topic,
action => Action,
permission => Permission
}
|| {Permission, Action, Topic} <- Rules
]
}. }.
atom(B) when is_binary(B) ->
try format_rules(Rules) ->
binary_to_existing_atom(B, utf8) [emqx_authz_rule_raw:format_rule(Rule) || Rule <- Rules].
catch
_Error:_Expection -> binary_to_atom(B)
end;
atom(A) when is_atom(A) -> A.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions

View File

@ -16,7 +16,6 @@
-module(emqx_authz_file). -module(emqx_authz_file).
-include("emqx_authz.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-behaviour(emqx_authz). -behaviour(emqx_authz).

View File

@ -51,6 +51,11 @@
?PH_CERT_CN_NAME ?PH_CERT_CN_NAME
]). ]).
-define(PLACEHOLDERS_FOR_RICH_ACTIONS, [
?PH_QOS,
?PH_RETAIN
]).
description() -> description() ->
"AuthZ with http". "AuthZ with http".
@ -72,7 +77,7 @@ destroy(#{annotations := #{id := Id}}) ->
authorize( authorize(
Client, Client,
PubSub, Action,
Topic, Topic,
#{ #{
type := http, type := http,
@ -81,7 +86,7 @@ authorize(
request_timeout := RequestTimeout request_timeout := RequestTimeout
} = Config } = Config
) -> ) ->
Request = generate_request(PubSub, Topic, Client, Config), Request = generate_request(Action, Topic, Client, Config),
case emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of case emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of
{ok, 204, _Headers} -> {ok, 204, _Headers} ->
{matched, allow}; {matched, allow};
@ -139,14 +144,14 @@ parse_config(
method => Method, method => Method,
base_url => BaseUrl, base_url => BaseUrl,
headers => Headers, headers => Headers,
base_path_templete => emqx_authz_utils:parse_str(Path, ?PLACEHOLDERS), base_path_templete => emqx_authz_utils:parse_str(Path, placeholders()),
base_query_template => emqx_authz_utils:parse_deep( base_query_template => emqx_authz_utils:parse_deep(
cow_qs:parse_qs(to_bin(Query)), cow_qs:parse_qs(to_bin(Query)),
?PLACEHOLDERS placeholders()
), ),
body_template => emqx_authz_utils:parse_deep( body_template => emqx_authz_utils:parse_deep(
maps:to_list(maps:get(body, Conf, #{})), maps:to_list(maps:get(body, Conf, #{})),
?PLACEHOLDERS placeholders()
), ),
request_timeout => ReqTimeout, request_timeout => ReqTimeout,
%% pool_type default value `random` %% pool_type default value `random`
@ -173,7 +178,7 @@ parse_url(Url) ->
end. end.
generate_request( generate_request(
PubSub, Action,
Topic, Topic,
Client, Client,
#{ #{
@ -184,7 +189,7 @@ generate_request(
body_template := BodyTemplate body_template := BodyTemplate
} }
) -> ) ->
Values = client_vars(Client, PubSub, Topic), Values = client_vars(Client, Action, Topic),
Path = emqx_authz_utils:render_urlencoded_str(BasePathTemplate, Values), Path = emqx_authz_utils:render_urlencoded_str(BasePathTemplate, Values),
Query = emqx_authz_utils:render_deep(BaseQueryTemplate, Values), Query = emqx_authz_utils:render_deep(BaseQueryTemplate, Values),
Body = emqx_authz_utils:render_deep(BodyTemplate, Values), Body = emqx_authz_utils:render_deep(BodyTemplate, Values),
@ -227,11 +232,9 @@ serialize_body(<<"application/json">>, Body) ->
serialize_body(<<"application/x-www-form-urlencoded">>, Body) -> serialize_body(<<"application/x-www-form-urlencoded">>, Body) ->
query_string(Body). query_string(Body).
client_vars(Client, PubSub, Topic) -> client_vars(Client, Action, Topic) ->
Client#{ Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
action => PubSub, Vars#{topic => Topic}.
topic => Topic
}.
to_list(A) when is_atom(A) -> to_list(A) when is_atom(A) ->
atom_to_list(A); atom_to_list(A);
@ -243,3 +246,11 @@ to_list(L) when is_list(L) ->
to_bin(B) when is_binary(B) -> B; to_bin(B) when is_binary(B) -> B;
to_bin(L) when is_list(L) -> list_to_binary(L); to_bin(L) when is_list(L) -> list_to_binary(L);
to_bin(X) -> X. to_bin(X) -> X.
placeholders() ->
placeholders(emqx_authz:feature_available(rich_actions)).
placeholders(true) ->
?PLACEHOLDERS ++ ?PLACEHOLDERS_FOR_RICH_ACTIONS;
placeholders(false) ->
?PLACEHOLDERS.

View File

@ -16,7 +16,6 @@
-module(emqx_authz_mnesia). -module(emqx_authz_mnesia).
-include_lib("emqx/include/emqx.hrl").
-include_lib("stdlib/include/ms_transform.hrl"). -include_lib("stdlib/include/ms_transform.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
@ -202,25 +201,16 @@ record_count() ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
normalize_rules(Rules) -> normalize_rules(Rules) ->
lists:map(fun normalize_rule/1, Rules). lists:flatmap(fun normalize_rule/1, Rules).
normalize_rule({Permission, Action, Topic}) -> normalize_rule(RuleRaw) ->
{normalize_permission(Permission), normalize_action(Action), normalize_topic(Topic)}; case emqx_authz_rule_raw:parse_rule(RuleRaw) of
normalize_rule(Rule) -> %% For backward compatibility
error({invalid_rule, Rule}). {ok, {Permission, Action, TopicFilters}} ->
[{Permission, Action, TopicFilter} || TopicFilter <- TopicFilters];
normalize_topic(Topic) when is_list(Topic) -> list_to_binary(Topic); {error, Reason} ->
normalize_topic(Topic) when is_binary(Topic) -> Topic; error(Reason)
normalize_topic(Topic) -> error({invalid_rule_topic, Topic}). end.
normalize_action(publish) -> publish;
normalize_action(subscribe) -> subscribe;
normalize_action(all) -> all;
normalize_action(Action) -> error({invalid_rule_action, Action}).
normalize_permission(allow) -> allow;
normalize_permission(deny) -> deny;
normalize_permission(Permission) -> error({invalid_rule_permission, Permission}).
do_get_rules(Key) -> do_get_rules(Key) ->
case mnesia:dirty_read(?ACL_TABLE, Key) of case mnesia:dirty_read(?ACL_TABLE, Key) of

View File

@ -68,7 +68,7 @@ destroy(#{annotations := #{id := Id}}) ->
authorize( authorize(
Client, Client,
PubSub, Action,
Topic, Topic,
#{ #{
collection := Collection, collection := Collection,
@ -97,15 +97,21 @@ authorize(
{ok, []} -> {ok, []} ->
nomatch; nomatch;
{ok, Rows} -> {ok, Rows} ->
Rules = [ Rules = lists:flatmap(fun parse_rule/1, Rows),
emqx_authz_rule:compile({Permission, all, Action, Topics}) do_authorize(Client, Action, Topic, Rules)
|| #{ end.
<<"topics">> := Topics,
<<"permission">> := Permission, parse_rule(Row) ->
<<"action">> := Action case emqx_authz_rule_raw:parse_rule(Row) of
} <- Rows {ok, {Permission, Action, Topics}} ->
], [emqx_authz_rule:compile({Permission, all, Action, Topics})];
do_authorize(Client, PubSub, Topic, Rules) {error, Reason} ->
?SLOG(error, #{
msg => "parse_rule_error",
reason => Reason,
row => Row
}),
[]
end. end.
do_authorize(_Client, _PubSub, _Topic, []) -> do_authorize(_Client, _PubSub, _Topic, []) ->

View File

@ -55,7 +55,7 @@ create(#{query := SQL} = Source0) ->
ResourceId = emqx_authz_utils:make_resource_id(?MODULE), ResourceId = emqx_authz_utils:make_resource_id(?MODULE),
Source = Source0#{prepare_statement => #{?PREPARE_KEY => PrepareSQL}}, Source = Source0#{prepare_statement => #{?PREPARE_KEY => PrepareSQL}},
{ok, _Data} = emqx_authz_utils:create_resource(ResourceId, emqx_mysql, Source), {ok, _Data} = emqx_authz_utils:create_resource(ResourceId, emqx_mysql, Source),
Source#{annotations => #{id => ResourceId, tmpl_oken => TmplToken}}. Source#{annotations => #{id => ResourceId, tmpl_token => TmplToken}}.
update(#{query := SQL} = Source0) -> update(#{query := SQL} = Source0) ->
{PrepareSQL, TmplToken} = emqx_authz_utils:parse_sql(SQL, '?', ?PLACEHOLDERS), {PrepareSQL, TmplToken} = emqx_authz_utils:parse_sql(SQL, '?', ?PLACEHOLDERS),
@ -64,7 +64,7 @@ update(#{query := SQL} = Source0) ->
{error, Reason} -> {error, Reason} ->
error({load_config_error, Reason}); error({load_config_error, Reason});
{ok, Id} -> {ok, Id} ->
Source#{annotations => #{id => Id, tmpl_oken => TmplToken}} Source#{annotations => #{id => Id, tmpl_token => TmplToken}}
end. end.
destroy(#{annotations := #{id := Id}}) -> destroy(#{annotations := #{id := Id}}) ->
@ -72,57 +72,51 @@ destroy(#{annotations := #{id := Id}}) ->
authorize( authorize(
Client, Client,
PubSub, Action,
Topic, Topic,
#{ #{
annotations := #{ annotations := #{
id := ResourceID, id := ResourceID,
tmpl_oken := TmplToken tmpl_token := TmplToken
} }
} }
) -> ) ->
RenderParams = emqx_authz_utils:render_sql_params(TmplToken, Client), Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
RenderParams = emqx_authz_utils:render_sql_params(TmplToken, Vars),
case case
emqx_resource:simple_sync_query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams}) emqx_resource:simple_sync_query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams})
of of
{ok, _Columns, []} -> {ok, _ColumnNames, []} ->
nomatch; nomatch;
{ok, Columns, Rows} -> {ok, ColumnNames, Rows} ->
do_authorize(Client, PubSub, Topic, Columns, Rows); do_authorize(Client, Action, Topic, ColumnNames, Rows);
{error, Reason} -> {error, Reason} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "query_mysql_error", msg => "query_mysql_error",
reason => Reason, reason => Reason,
tmpl_oken => TmplToken, tmpl_token => TmplToken,
params => RenderParams, params => RenderParams,
resource_id => ResourceID resource_id => ResourceID
}), }),
nomatch nomatch
end. end.
do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> do_authorize(_Client, _Action, _Topic, _ColumnNames, []) ->
nomatch; nomatch;
do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> do_authorize(Client, Action, Topic, ColumnNames, [Row | Tail]) ->
case try
emqx_authz_rule:match( emqx_authz_rule:match(
Client, Client, Action, Topic, emqx_authz_utils:parse_rule_from_row(ColumnNames, Row)
PubSub,
Topic,
emqx_authz_rule:compile(format_result(Columns, Row))
) )
of of
{matched, Permission} -> {matched, Permission}; {matched, Permission} -> {matched, Permission};
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) nomatch -> do_authorize(Client, Action, Topic, ColumnNames, Tail)
catch
error:Reason ->
?SLOG(error, #{
msg => "match_rule_error",
reason => Reason,
rule => Row
}),
do_authorize(Client, Action, Topic, ColumnNames, Tail)
end. end.
format_result(Columns, Row) ->
Permission = lists:nth(index(<<"permission">>, Columns), Row),
Action = lists:nth(index(<<"action">>, Columns), Row),
Topic = lists:nth(index(<<"topic">>, Columns), Row),
{Permission, all, Action, [Topic]}.
index(Elem, List) ->
index(Elem, List, 1).
index(_Elem, [], _Index) -> {error, not_found};
index(Elem, [Elem | _List], Index) -> Index;
index(Elem, [_ | List], Index) -> index(Elem, List, Index + 1).

View File

@ -21,6 +21,8 @@
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("epgsql/include/epgsql.hrl").
-behaviour(emqx_authz). -behaviour(emqx_authz).
%% AuthZ Callbacks %% AuthZ Callbacks
@ -77,7 +79,7 @@ destroy(#{annotations := #{id := Id}}) ->
authorize( authorize(
Client, Client,
PubSub, Action,
Topic, Topic,
#{ #{
annotations := #{ annotations := #{
@ -86,14 +88,15 @@ authorize(
} }
} }
) -> ) ->
RenderedParams = emqx_authz_utils:render_sql_params(Placeholders, Client), Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
RenderedParams = emqx_authz_utils:render_sql_params(Placeholders, Vars),
case case
emqx_resource:simple_sync_query(ResourceID, {prepared_query, ResourceID, RenderedParams}) emqx_resource:simple_sync_query(ResourceID, {prepared_query, ResourceID, RenderedParams})
of of
{ok, _Columns, []} -> {ok, _Columns, []} ->
nomatch; nomatch;
{ok, Columns, Rows} -> {ok, Columns, Rows} ->
do_authorize(Client, PubSub, Topic, Columns, Rows); do_authorize(Client, Action, Topic, column_names(Columns), Rows);
{error, Reason} -> {error, Reason} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "query_postgresql_error", msg => "query_postgresql_error",
@ -104,33 +107,29 @@ authorize(
nomatch nomatch
end. end.
do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> do_authorize(_Client, _Action, _Topic, _ColumnNames, []) ->
nomatch; nomatch;
do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> do_authorize(Client, Action, Topic, ColumnNames, [Row | Tail]) ->
case try
emqx_authz_rule:match( emqx_authz_rule:match(
Client, Client, Action, Topic, emqx_authz_utils:parse_rule_from_row(ColumnNames, Row)
PubSub,
Topic,
emqx_authz_rule:compile(format_result(Columns, Row))
) )
of of
{matched, Permission} -> {matched, Permission}; {matched, Permission} -> {matched, Permission};
nomatch -> do_authorize(Client, PubSub, Topic, Columns, Tail) nomatch -> do_authorize(Client, Action, Topic, ColumnNames, Tail)
catch
error:Reason:Stack ->
?SLOG(error, #{
msg => "match_rule_error",
reason => Reason,
rule => Row,
stack => Stack
}),
do_authorize(Client, Action, Topic, ColumnNames, Tail)
end. end.
format_result(Columns, Row) -> column_names(Columns) ->
Permission = lists:nth(index(<<"permission">>, 2, Columns), erlang:tuple_to_list(Row)), lists:map(
Action = lists:nth(index(<<"action">>, 2, Columns), erlang:tuple_to_list(Row)), fun(#column{name = Name}) -> Name end,
Topic = lists:nth(index(<<"topic">>, 2, Columns), erlang:tuple_to_list(Row)), Columns
{Permission, all, Action, [Topic]}. ).
index(Key, N, TupleList) when is_integer(N) ->
Tuple = lists:keyfind(Key, N, TupleList),
index(Tuple, TupleList, 1);
index(_Tuple, [], _Index) ->
{error, not_found};
index(Tuple, [Tuple | _TupleList], Index) ->
Index;
index(Tuple, [_ | TupleList], Index) ->
index(Tuple, TupleList, Index + 1).

View File

@ -70,19 +70,20 @@ destroy(#{annotations := #{id := Id}}) ->
authorize( authorize(
Client, Client,
PubSub, Action,
Topic, Topic,
#{ #{
cmd_template := CmdTemplate, cmd_template := CmdTemplate,
annotations := #{id := ResourceID} annotations := #{id := ResourceID}
} }
) -> ) ->
Cmd = emqx_authz_utils:render_deep(CmdTemplate, Client), Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
Cmd = emqx_authz_utils:render_deep(CmdTemplate, Vars),
case emqx_resource:simple_sync_query(ResourceID, {cmd, Cmd}) of case emqx_resource:simple_sync_query(ResourceID, {cmd, Cmd}) of
{ok, []} -> {ok, []} ->
nomatch; nomatch;
{ok, Rows} -> {ok, Rows} ->
do_authorize(Client, PubSub, Topic, Rows); do_authorize(Client, Action, Topic, Rows);
{error, Reason} -> {error, Reason} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "query_redis_error", msg => "query_redis_error",
@ -93,21 +94,60 @@ authorize(
nomatch nomatch
end. end.
do_authorize(_Client, _PubSub, _Topic, []) -> do_authorize(_Client, _Action, _Topic, []) ->
nomatch; nomatch;
do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> do_authorize(Client, Action, Topic, [TopicFilterRaw, RuleEncoded | Tail]) ->
case try
emqx_authz_rule:match( emqx_authz_rule:match(
Client, Client,
PubSub, Action,
Topic, Topic,
emqx_authz_rule:compile({allow, all, Action, [TopicFilter]}) compile_rule(RuleEncoded, TopicFilterRaw)
) )
of of
{matched, Permission} -> {matched, Permission}; {matched, Permission} -> {matched, Permission};
nomatch -> do_authorize(Client, PubSub, Topic, Tail) nomatch -> do_authorize(Client, Action, Topic, Tail)
catch
error:Reason ->
?SLOG(error, #{
msg => "match_rule_error",
reason => Reason,
rule_encoded => RuleEncoded,
topic_filter_raw => TopicFilterRaw
}),
do_authorize(Client, Action, Topic, Tail)
end.
compile_rule(RuleBin, TopicFilterRaw) ->
RuleRaw =
maps:merge(
#{
<<"permission">> => <<"allow">>,
<<"topic">> => TopicFilterRaw
},
parse_rule(RuleBin)
),
case emqx_authz_rule_raw:parse_rule(RuleRaw) of
{ok, {Permission, Action, Topics}} ->
emqx_authz_rule:compile({Permission, all, Action, Topics});
{error, Reason} ->
error(Reason)
end. end.
tokens(Query) -> tokens(Query) ->
Tokens = binary:split(Query, <<" ">>, [global]), Tokens = binary:split(Query, <<" ">>, [global]),
[Token || Token <- Tokens, size(Token) > 0]. [Token || Token <- Tokens, size(Token) > 0].
parse_rule(<<"publish">>) ->
#{<<"action">> => <<"publish">>};
parse_rule(<<"subscribe">>) ->
#{<<"action">> => <<"subscribe">>};
parse_rule(<<"all">>) ->
#{<<"action">> => <<"all">>};
parse_rule(Bin) when is_binary(Bin) ->
case emqx_utils_json:safe_decode(Bin, [return_maps]) of
{ok, Map} when is_map(Map) ->
maps:with([<<"qos">>, <<"action">>, <<"retain">>], Map);
{error, Error} ->
error({invalid_topic_rule, Bin, Error})
end.

View File

@ -16,9 +16,9 @@
-module(emqx_authz_rule). -module(emqx_authz_rule).
-include("emqx_authz.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
-include("emqx_authz.hrl").
-ifdef(TEST). -ifdef(TEST).
-compile(export_all). -compile(export_all).
@ -32,50 +32,123 @@
compile/1 compile/1
]). ]).
-type ipaddress() :: -type permission() :: allow | deny.
{ipaddr, esockd_cidr:cidr_string()}
| {ipaddrs, list(esockd_cidr:cidr_string())}.
-type username() :: {username, binary()}. -type who_condition() ::
-type clientid() :: {clientid, binary()}.
-type who() ::
ipaddress() ipaddress()
| username() | username()
| clientid() | clientid()
| {'and', [ipaddress() | username() | clientid()]} | {'and', [ipaddress() | username() | clientid()]}
| {'or', [ipaddress() | username() | clientid()]} | {'or', [ipaddress() | username() | clientid()]}
| all. | all.
-type ipaddress() ::
{ipaddr, esockd_cidr:cidr_string()}
| {ipaddrs, list(esockd_cidr:cidr_string())}.
-type username() :: {username, binary()}.
-type clientid() :: {clientid, binary()}.
-type action() :: subscribe | publish | all. -type action_condition() ::
-type permission() :: allow | deny. subscribe
| publish
| #{action_type := subscribe, qos := qos_condition()}
| #{action_type := publish | all, qos := qos_condition(), retain := retain_condition()}
| all.
-type qos_condition() :: [qos()].
-type retain_condition() :: retain() | all.
-type rule() :: {permission(), who(), action(), list(emqx_types:topic())}. -type topic_condition() :: list(emqx_types:topic() | {eq, emqx_types:topic()}).
-type rule() :: {permission(), who_condition(), action_condition(), topic_condition()}.
-type qos() :: 0..2.
-type retain() :: boolean().
-type action() ::
#{action_type := subscribe, qos := qos()}
| #{action_type := publish, qos := qos(), retain := retain()}.
-export_type([ -export_type([
action/0, permission/0,
permission/0 who_condition/0,
action_condition/0,
topic_condition/0
]). ]).
-define(IS_PERMISSION(Permission), (Permission =:= allow orelse Permission =:= deny)).
compile({Permission, all}) when compile({Permission, all}) when
?ALLOW_DENY(Permission) ?IS_PERMISSION(Permission)
-> ->
{Permission, all, all, [compile_topic(<<"#">>)]}; {Permission, all, all, [compile_topic(<<"#">>)]};
compile({Permission, Who, Action, TopicFilters}) when compile({Permission, Who, Action, TopicFilters}) when
?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(TopicFilters) ?IS_PERMISSION(Permission) andalso is_list(TopicFilters)
-> ->
{atom(Permission), compile_who(Who), atom(Action), [ {Permission, compile_who(Who), compile_action(Action), [
compile_topic(Topic) compile_topic(Topic)
|| Topic <- TopicFilters || Topic <- TopicFilters
]}; ]};
compile({Permission, _Who, _Action, _TopicFilter}) when not ?ALLOW_DENY(Permission) -> compile({Permission, _Who, _Action, _TopicFilter}) when not ?IS_PERMISSION(Permission) ->
throw({invalid_authorization_permission, Permission}); throw({invalid_authorization_permission, Permission});
compile({_Permission, _Who, Action, _TopicFilter}) when not ?PUBSUB(Action) ->
throw({invalid_authorization_action, Action});
compile(BadRule) -> compile(BadRule) ->
throw({invalid_authorization_rule, BadRule}). throw({invalid_authorization_rule, BadRule}).
compile_action(Action) ->
compile_action(emqx_authz:feature_available(rich_actions), Action).
-define(IS_ACTION_WITH_RETAIN(Action), (Action =:= publish orelse Action =:= all)).
compile_action(_RichActionsOn, subscribe) ->
subscribe;
compile_action(_RichActionsOn, Action) when ?IS_ACTION_WITH_RETAIN(Action) ->
Action;
compile_action(true = _RichActionsOn, {subscribe, Opts}) when is_list(Opts) ->
#{
action_type => subscribe,
qos => qos_from_opts(Opts)
};
compile_action(true = _RichActionsOn, {Action, Opts}) when
?IS_ACTION_WITH_RETAIN(Action) andalso is_list(Opts)
->
#{
action_type => Action,
qos => qos_from_opts(Opts),
retain => retain_from_opts(Opts)
};
compile_action(_RichActionsOn, Action) ->
throw({invalid_authorization_action, Action}).
qos_from_opts(Opts) ->
try
case proplists:get_all_values(qos, Opts) of
[] ->
?DEFAULT_RULE_QOS;
QoSs ->
lists:flatmap(
fun
(QoS) when is_integer(QoS) ->
[validate_qos(QoS)];
(QoS) when is_list(QoS) ->
lists:map(fun validate_qos/1, QoS)
end,
QoSs
)
end
catch
bad_qos ->
throw({invalid_authorization_qos, Opts})
end.
validate_qos(QoS) when is_integer(QoS), QoS >= 0, QoS =< 2 ->
QoS;
validate_qos(_) ->
throw(bad_qos).
retain_from_opts(Opts) ->
case proplists:get_value(retain, Opts, ?DEFAULT_RULE_RETAIN) of
all -> all;
Retain when is_boolean(Retain) -> Retain;
_ -> throw({invalid_authorization_retain, Opts})
end.
compile_who(all) -> compile_who(all) ->
all; all;
compile_who({user, Username}) -> compile_who({user, Username}) ->
@ -99,8 +172,12 @@ compile_who({ipaddrs, CIDRs}) ->
compile_who({'and', L}) when is_list(L) -> compile_who({'and', L}) when is_list(L) ->
{'and', [compile_who(Who) || Who <- L]}; {'and', [compile_who(Who) || Who <- L]};
compile_who({'or', L}) when is_list(L) -> compile_who({'or', L}) when is_list(L) ->
{'or', [compile_who(Who) || Who <- L]}. {'or', [compile_who(Who) || Who <- L]};
compile_who(Who) ->
throw({invalid_who, Who}).
compile_topic("eq " ++ Topic) ->
{eq, emqx_topic:words(bin(Topic))};
compile_topic(<<"eq ", Topic/binary>>) -> compile_topic(<<"eq ", Topic/binary>>) ->
{eq, emqx_topic:words(Topic)}; {eq, emqx_topic:words(Topic)};
compile_topic({eq, Topic}) -> compile_topic({eq, Topic}) ->
@ -117,45 +194,65 @@ compile_topic(Topic) ->
Tokens -> {pattern, Tokens} Tokens -> {pattern, Tokens}
end. 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) -> bin(L) when is_list(L) ->
list_to_binary(L); list_to_binary(L);
bin(B) when is_binary(B) -> bin(B) when is_binary(B) ->
B. B.
-spec matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()]) -> -spec matches(emqx_types:clientinfo(), action(), emqx_types:topic(), [rule()]) ->
{matched, allow} | {matched, deny} | nomatch. {matched, allow} | {matched, deny} | nomatch.
matches(_Client, _PubSub, _Topic, []) -> matches(_Client, _Action, _Topic, []) ->
nomatch; nomatch;
matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) -> matches(Client, Action, Topic, [{Permission, WhoCond, ActionCond, TopicCond} | Tail]) ->
case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of case match(Client, Action, Topic, {Permission, WhoCond, ActionCond, TopicCond}) of
nomatch -> matches(Client, PubSub, Topic, Tail); nomatch -> matches(Client, Action, Topic, Tail);
Matched -> Matched Matched -> Matched
end. end.
-spec match(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), rule()) -> -spec match(emqx_types:clientinfo(), action(), emqx_types:topic(), rule()) ->
{matched, allow} | {matched, deny} | nomatch. {matched, allow} | {matched, deny} | nomatch.
match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) -> match(Client, Action, Topic, {Permission, WhoCond, ActionCond, TopicCond}) ->
case case
match_action(PubSub, Action) andalso match_action(Action, ActionCond) andalso
match_who(Client, Who) andalso match_who(Client, WhoCond) andalso
match_topics(Client, Topic, TopicFilters) match_topics(Client, Topic, TopicCond)
of of
true -> {matched, Permission}; true -> {matched, Permission};
_ -> nomatch _ -> nomatch
end. end.
match_action(publish, publish) -> true; -spec match_action(action(), action_condition()) -> boolean().
match_action(subscribe, subscribe) -> true; match_action(#{action_type := publish}, PubSubCond) when is_atom(PubSubCond) ->
match_action(_, all) -> true; match_pubsub(publish, PubSubCond);
match_action(_, _) -> false. match_action(
#{action_type := publish, qos := QoS, retain := Retain}, #{
action_type := publish, qos := QoSCond, retain := RetainCond
}
) ->
match_qos(QoS, QoSCond) andalso match_retain(Retain, RetainCond);
match_action(#{action_type := publish, qos := QoS, retain := Retain}, #{
action_type := all, qos := QoSCond, retain := RetainCond
}) ->
match_qos(QoS, QoSCond) andalso match_retain(Retain, RetainCond);
match_action(#{action_type := subscribe}, PubSubCond) when is_atom(PubSubCond) ->
match_pubsub(subscribe, PubSubCond);
match_action(#{action_type := subscribe, qos := QoS}, #{action_type := subscribe, qos := QoSCond}) ->
match_qos(QoS, QoSCond);
match_action(#{action_type := subscribe, qos := QoS}, #{action_type := all, qos := QoSCond}) ->
match_qos(QoS, QoSCond);
match_action(_, _) ->
false.
match_pubsub(publish, publish) -> true;
match_pubsub(subscribe, subscribe) -> true;
match_pubsub(_, all) -> true;
match_pubsub(_, _) -> false.
match_qos(QoS, QoSs) -> lists:member(QoS, QoSs).
match_retain(_, all) -> true;
match_retain(Retain, Retain) when is_boolean(Retain) -> true;
match_retain(_, _) -> false.
match_who(_, all) -> match_who(_, all) ->
true; true;

View File

@ -0,0 +1,197 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2021-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.
%%
%% @doc
%% This module converts authz rule fields obtained from
%% external sources like database or API to the format
%% accepted by emqx_authz_rule module.
%%--------------------------------------------------------------------
-module(emqx_authz_rule_raw).
-export([parse_rule/1, format_rule/1]).
-include("emqx_authz.hrl").
-type rule_raw() :: #{binary() => binary() | [binary()]}.
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
-spec parse_rule(rule_raw()) ->
{ok, {
emqx_authz_rule:permission(),
emqx_authz_rule:action_condition(),
emqx_authz_rule:topic_condition()
}}
| {error, term()}.
parse_rule(
#{
<<"permission">> := PermissionRaw,
<<"action">> := ActionTypeRaw
} = RuleRaw
) ->
try
Topics = validate_rule_topics(RuleRaw),
Permission = validate_rule_permission(PermissionRaw),
ActionType = validate_rule_action_type(ActionTypeRaw),
Action = validate_rule_action(ActionType, RuleRaw),
{ok, {Permission, Action, Topics}}
catch
throw:ValidationError ->
{error, ValidationError}
end;
parse_rule(RuleRaw) ->
{error, {invalid_rule, RuleRaw}}.
-spec format_rule({
emqx_authz_rule:permission(),
emqx_authz_rule:action_condition(),
emqx_authz_rule:topic_condition()
}) -> map().
format_rule({Permission, Action, Topics}) when is_list(Topics) ->
maps:merge(
#{
topic => lists:map(fun format_topic/1, Topics),
permission => Permission
},
format_action(Action)
);
format_rule({Permission, Action, Topic}) ->
maps:merge(
#{
topic => format_topic(Topic),
permission => Permission
},
format_action(Action)
).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
validate_rule_topics(#{<<"topic">> := TopicRaw}) when is_binary(TopicRaw) ->
[validate_rule_topic(TopicRaw)];
validate_rule_topics(#{<<"topics">> := TopicsRaw}) when is_list(TopicsRaw) ->
lists:map(fun validate_rule_topic/1, TopicsRaw);
validate_rule_topics(RuleRaw) ->
throw({invalid_topics, RuleRaw}).
validate_rule_topic(<<"eq ", TopicRaw/binary>>) ->
{eq, validate_rule_topic(TopicRaw)};
validate_rule_topic(TopicRaw) when is_binary(TopicRaw) -> TopicRaw.
validate_rule_permission(<<"allow">>) -> allow;
validate_rule_permission(<<"deny">>) -> deny;
validate_rule_permission(PermissionRaw) -> throw({invalid_permission, PermissionRaw}).
validate_rule_action_type(<<"publish">>) -> publish;
validate_rule_action_type(<<"subscribe">>) -> subscribe;
validate_rule_action_type(<<"all">>) -> all;
validate_rule_action_type(ActionRaw) -> throw({invalid_action, ActionRaw}).
validate_rule_action(ActionType, RuleRaw) ->
validate_rule_action(emqx_authz:feature_available(rich_actions), ActionType, RuleRaw).
%% rich_actions disabled
validate_rule_action(false, ActionType, _RuleRaw) ->
ActionType;
%% rich_actions enabled
validate_rule_action(true, publish, RuleRaw) ->
Qos = validate_rule_qos(maps:get(<<"qos">>, RuleRaw, ?DEFAULT_RULE_QOS)),
Retain = validate_rule_retain(maps:get(<<"retain">>, RuleRaw, <<"all">>)),
{publish, [{qos, Qos}, {retain, Retain}]};
validate_rule_action(true, subscribe, RuleRaw) ->
Qos = validate_rule_qos(maps:get(<<"qos">>, RuleRaw, ?DEFAULT_RULE_QOS)),
{subscribe, [{qos, Qos}]};
validate_rule_action(true, all, RuleRaw) ->
Qos = validate_rule_qos(maps:get(<<"qos">>, RuleRaw, ?DEFAULT_RULE_QOS)),
Retain = validate_rule_retain(maps:get(<<"retain">>, RuleRaw, <<"all">>)),
{all, [{qos, Qos}, {retain, Retain}]}.
validate_rule_qos(QosInt) when is_integer(QosInt) andalso QosInt >= 0 andalso QosInt =< 2 ->
[QosInt];
validate_rule_qos(QosBin) when is_binary(QosBin) ->
try
QosRawList = binary:split(QosBin, <<",">>, [global]),
lists:map(fun validate_rule_qos_atomic/1, QosRawList)
catch
_:_ ->
throw({invalid_qos, QosBin})
end;
validate_rule_qos(QosList) when is_list(QosList) ->
try
lists:map(fun validate_rule_qos_atomic/1, QosList)
catch
invalid_qos ->
throw({invalid_qos, QosList})
end;
validate_rule_qos(undefined) ->
?DEFAULT_RULE_QOS;
validate_rule_qos(null) ->
?DEFAULT_RULE_QOS;
validate_rule_qos(QosRaw) ->
throw({invalid_qos, QosRaw}).
validate_rule_qos_atomic(<<"0">>) -> 0;
validate_rule_qos_atomic(<<"1">>) -> 1;
validate_rule_qos_atomic(<<"2">>) -> 2;
validate_rule_qos_atomic(0) -> 0;
validate_rule_qos_atomic(1) -> 1;
validate_rule_qos_atomic(2) -> 2;
validate_rule_qos_atomic(_) -> throw(invalid_qos).
validate_rule_retain(<<"0">>) -> false;
validate_rule_retain(<<"1">>) -> true;
validate_rule_retain(0) -> false;
validate_rule_retain(1) -> true;
validate_rule_retain(<<"true">>) -> true;
validate_rule_retain(<<"false">>) -> false;
validate_rule_retain(true) -> true;
validate_rule_retain(false) -> false;
validate_rule_retain(undefined) -> ?DEFAULT_RULE_RETAIN;
validate_rule_retain(null) -> ?DEFAULT_RULE_RETAIN;
validate_rule_retain(<<"all">>) -> ?DEFAULT_RULE_RETAIN;
validate_rule_retain(Retain) -> throw({invalid_retain, Retain}).
format_action(Action) ->
format_action(emqx_authz:feature_available(rich_actions), Action).
%% rich_actions disabled
format_action(false, Action) when is_atom(Action) ->
#{
action => Action
};
format_action(false, {ActionType, _Opts}) ->
#{
action => ActionType
};
%% rich_actions enabled
format_action(true, Action) when is_atom(Action) ->
#{
action => Action
};
format_action(true, {ActionType, Opts}) ->
#{
action => ActionType,
qos => proplists:get_value(qos, Opts, ?DEFAULT_RULE_QOS),
retain => proplists:get_value(retain, Opts, ?DEFAULT_RULE_RETAIN)
}.
format_topic({eq, Topic}) when is_binary(Topic) ->
<<"eq ", Topic/binary>>;
format_topic(Topic) when is_binary(Topic) ->
Topic.

View File

@ -31,7 +31,10 @@
parse_sql/3, parse_sql/3,
render_deep/2, render_deep/2,
render_str/2, render_str/2,
render_sql_params/2 render_sql_params/2,
client_vars/1,
vars_for_rule_query/2,
parse_rule_from_row/2
]). ]).
-export([ -export([
@ -43,6 +46,8 @@
start_after_created => false start_after_created => false
}). }).
-include_lib("emqx/include/logger.hrl").
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% APIs %% APIs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -171,6 +176,24 @@ content_type(Headers) when is_list(Headers) ->
<<"application/json">> <<"application/json">>
). ).
-define(RAW_RULE_KEYS, [<<"permission">>, <<"action">>, <<"topic">>, <<"qos">>, <<"retain">>]).
parse_rule_from_row(ColumnNames, Row) ->
RuleRaw = maps:with(?RAW_RULE_KEYS, maps:from_list(lists:zip(ColumnNames, to_list(Row)))),
case emqx_authz_rule_raw:parse_rule(RuleRaw) of
{ok, {Permission, Action, Topics}} ->
emqx_authz_rule:compile({Permission, all, Action, Topics});
{error, Reason} ->
error(Reason)
end.
vars_for_rule_query(Client, ?authz_action(PubSub, Qos) = Action) ->
Client#{
action => PubSub,
qos => Qos,
retain => maps:get(retain, Action, false)
}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -208,3 +231,8 @@ handle_sql_var(_Name, Value) ->
bin(A) when is_atom(A) -> atom_to_binary(A, utf8); bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(L) when is_list(L) -> list_to_binary(L); bin(L) when is_list(L) -> list_to_binary(L);
bin(X) -> X. bin(X) -> X.
to_list(Tuple) when is_tuple(Tuple) ->
tuple_to_list(Tuple);
to_list(List) when is_list(List) ->
List.

View File

@ -96,7 +96,7 @@ t_api(_) ->
<<"hasnext">> := false <<"hasnext">> := false
} }
} = emqx_utils_json:decode(Request1), } = emqx_utils_json:decode(Request1),
?assertEqual(3, length(Rules1)), ?assertEqual(?USERNAME_RULES_EXAMPLE_COUNT, length(Rules1)),
{ok, 200, Request1_1} = {ok, 200, Request1_1} =
request( request(
@ -204,7 +204,7 @@ t_api(_) ->
} = } =
emqx_utils_json:decode(Request4), emqx_utils_json:decode(Request4),
#{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3} = emqx_utils_json:decode(Request5), #{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3} = emqx_utils_json:decode(Request5),
?assertEqual(3, length(Rules3)), ?assertEqual(?CLIENTID_RULES_EXAMPLE_COUNT, length(Rules3)),
{ok, 204, _} = {ok, 204, _} =
request( request(
@ -253,7 +253,7 @@ t_api(_) ->
[] []
), ),
#{<<"rules">> := Rules5} = emqx_utils_json:decode(Request7), #{<<"rules">> := Rules5} = emqx_utils_json:decode(Request7),
?assertEqual(3, length(Rules5)), ?assertEqual(?ALL_RULES_EXAMPLE_COUNT, length(Rules5)),
{ok, 204, _} = {ok, 204, _} =
request( request(

View File

@ -42,11 +42,11 @@ init_per_suite(Config) ->
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok. ok = emqx_authz_test_lib:restore_authorizers().
init_per_testcase(TestCase, Config) -> init_per_testcase(TestCase, Config) ->
Apps = emqx_cth_suite:start( Apps = emqx_cth_suite:start(
[{emqx_conf, "authorization.no_match = deny"}, emqx_authz], [{emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"}, emqx_authz],
#{work_dir => filename:join(?config(priv_dir, Config), TestCase)} #{work_dir => filename:join(?config(priv_dir, Config), TestCase)}
), ),
[{tc_apps, Apps} | Config]. [{tc_apps, Apps} | Config].
@ -59,13 +59,7 @@ end_per_testcase(_TestCase, Config) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_ok(_Config) -> t_ok(_Config) ->
ClientInfo = #{ ClientInfo = emqx_authz_test_lib:base_client_info(),
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_config(?RAW_SOURCE#{ ok = setup_config(?RAW_SOURCE#{
<<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">> <<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">>
@ -73,23 +67,52 @@ t_ok(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
?assertEqual( ?assertEqual(
deny, deny,
emqx_access_control:authorize(ClientInfo, subscribe, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t">>)
).
t_rich_actions(_Config) ->
ClientInfo = emqx_authz_test_lib:base_client_info(),
ok = setup_config(?RAW_SOURCE#{
<<"rules">> =>
<<"{allow, {user, \"username\"}, {publish, [{qos, 1}, {retain, false}]}, [\"t\"]}.">>
}),
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t">>)
),
?assertEqual(
deny,
emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(0, false), <<"t">>)
),
?assertEqual(
deny,
emqx_access_control:authorize(ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t">>)
).
t_no_rich_actions(_Config) ->
_ = emqx_authz:set_feature_available(rich_actions, false),
?assertMatch(
{error, {pre_config_update, emqx_authz, {invalid_authorization_action, _}}},
emqx_authz:update(?CMD_REPLACE, [
?RAW_SOURCE#{
<<"rules">> =>
<<"{allow, {user, \"username\"}, {publish, [{qos, 1}, {retain, false}]}, [\"t\"]}.">>
}
])
). ).
t_superuser(_Config) -> t_superuser(_Config) ->
ClientInfo = #{ ClientInfo =
clientid => <<"clientid">>, emqx_authz_test_lib:client_info(#{is_superuser => true}),
username => <<"username">>,
is_superuser => true,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
%% no rules apply to superuser %% no rules apply to superuser
ok = setup_config(?RAW_SOURCE#{ ok = setup_config(?RAW_SOURCE#{
@ -98,12 +121,12 @@ t_superuser(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, subscribe, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t">>)
). ).
t_invalid_file(_Config) -> t_invalid_file(_Config) ->

View File

@ -65,6 +65,7 @@ init_per_testcase(_Case, Config) ->
Config. Config.
end_per_testcase(_Case, _Config) -> end_per_testcase(_Case, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
try try
ok = emqx_authz_http_test_server:stop() ok = emqx_authz_http_test_server:stop()
catch catch
@ -97,7 +98,7 @@ t_response_handling(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
%% Not OK, get, no body %% Not OK, get, no body
@ -109,7 +110,7 @@ t_response_handling(_Config) ->
#{} #{}
), ),
deny = emqx_access_control:authorize(ClientInfo, publish, <<"t">>), deny = emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>),
%% OK, get, 204 %% OK, get, 204
ok = setup_handler_and_config( ok = setup_handler_and_config(
@ -122,7 +123,7 @@ t_response_handling(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
%% Not OK, get, 400 %% Not OK, get, 400
@ -136,7 +137,7 @@ t_response_handling(_Config) ->
?assertEqual( ?assertEqual(
deny, deny,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
%% Not OK, get, 400 + body & headers %% Not OK, get, 400 + body & headers
@ -155,7 +156,7 @@ t_response_handling(_Config) ->
?assertEqual( ?assertEqual(
deny, deny,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
%% the server cannot be reached; should skip to the next %% the server cannot be reached; should skip to the next
@ -165,7 +166,7 @@ t_response_handling(_Config) ->
?check_trace( ?check_trace(
?assertEqual( ?assertEqual(
deny, deny,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
fun(Trace) -> fun(Trace) ->
?assertMatch( ?assertMatch(
@ -200,7 +201,9 @@ t_query_params(_Config) ->
proto_name := <<"MQTT">>, proto_name := <<"MQTT">>,
mountpoint := <<"MOUNTPOINT">>, mountpoint := <<"MOUNTPOINT">>,
topic := <<"t/1">>, topic := <<"t/1">>,
action := <<"publish">> action := <<"publish">>,
qos := <<"1">>,
retain := <<"false">>
} = cowboy_req:match_qs( } = cowboy_req:match_qs(
[ [
username, username,
@ -209,7 +212,9 @@ t_query_params(_Config) ->
proto_name, proto_name,
mountpoint, mountpoint,
topic, topic,
action action,
qos,
retain
], ],
Req0 Req0
), ),
@ -224,7 +229,9 @@ t_query_params(_Config) ->
"proto_name=${proto_name}&" "proto_name=${proto_name}&"
"mountpoint=${mountpoint}&" "mountpoint=${mountpoint}&"
"topic=${topic}&" "topic=${topic}&"
"action=${action}" "action=${action}&"
"qos=${qos}&"
"retain=${retain}"
>> >>
} }
), ),
@ -241,7 +248,7 @@ t_query_params(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t/1">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t/1">>)
). ).
t_path(_Config) -> t_path(_Config) ->
@ -256,7 +263,9 @@ t_path(_Config) ->
"MQTT/" "MQTT/"
"MOUNTPOINT/" "MOUNTPOINT/"
"t%2F1/" "t%2F1/"
"publish" "publish/"
"1/"
"false"
>>, >>,
cowboy_req:path(Req0) cowboy_req:path(Req0)
), ),
@ -271,7 +280,9 @@ t_path(_Config) ->
"${proto_name}/" "${proto_name}/"
"${mountpoint}/" "${mountpoint}/"
"${topic}/" "${topic}/"
"${action}" "${action}/"
"${qos}/"
"${retain}"
>> >>
} }
), ),
@ -288,7 +299,7 @@ t_path(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t/1">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t/1">>)
). ).
t_json_body(_Config) -> t_json_body(_Config) ->
@ -309,7 +320,9 @@ t_json_body(_Config) ->
<<"proto_name">> := <<"MQTT">>, <<"proto_name">> := <<"MQTT">>,
<<"mountpoint">> := <<"MOUNTPOINT">>, <<"mountpoint">> := <<"MOUNTPOINT">>,
<<"topic">> := <<"t">>, <<"topic">> := <<"t">>,
<<"action">> := <<"publish">> <<"action">> := <<"publish">>,
<<"qos">> := <<"1">>,
<<"retain">> := <<"false">>
}, },
emqx_utils_json:decode(RawBody, [return_maps]) emqx_utils_json:decode(RawBody, [return_maps])
), ),
@ -324,7 +337,9 @@ t_json_body(_Config) ->
<<"proto_name">> => <<"${proto_name}">>, <<"proto_name">> => <<"${proto_name}">>,
<<"mountpoint">> => <<"${mountpoint}">>, <<"mountpoint">> => <<"${mountpoint}">>,
<<"topic">> => <<"${topic}">>, <<"topic">> => <<"${topic}">>,
<<"action">> => <<"${action}">> <<"action">> => <<"${action}">>,
<<"qos">> => <<"${qos}">>,
<<"retain">> => <<"${retain}">>
} }
} }
), ),
@ -341,7 +356,45 @@ t_json_body(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t">>)
).
t_no_rich_actions(_Config) ->
_ = emqx_authz:set_feature_available(rich_actions, false),
ok = setup_handler_and_config(
fun(Req0, State) ->
?assertEqual(
<<"/authz/users/">>,
cowboy_req:path(Req0)
),
{ok, RawBody, Req1} = cowboy_req:read_body(Req0),
%% No interpolation if rich_actions is disabled
?assertMatch(
#{
<<"qos">> := <<"${qos}">>,
<<"retain">> := <<"${retain}">>
},
emqx_utils_json:decode(RawBody, [return_maps])
),
{ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
end,
#{
<<"method">> => <<"post">>,
<<"body">> => #{
<<"qos">> => <<"${qos}">>,
<<"retain">> => <<"${retain}">>
}
}
),
ClientInfo = emqx_authz_test_lib:base_client_info(),
?assertEqual(
allow,
emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t">>)
). ).
t_placeholder_and_body(_Config) -> t_placeholder_and_body(_Config) ->
@ -401,7 +454,7 @@ t_placeholder_and_body(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
). ).
t_no_value_for_placeholder(_Config) -> t_no_value_for_placeholder(_Config) ->
@ -441,7 +494,7 @@ t_no_value_for_placeholder(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
). ).
t_create_replace(_Config) -> t_create_replace(_Config) ->
@ -466,7 +519,7 @@ t_create_replace(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
%% Changing to valid config %% Changing to valid config
@ -485,7 +538,7 @@ t_create_replace(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
). ).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -22,6 +22,7 @@
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("emqx_authn/include/emqx_authn.hrl"). -include_lib("emqx_authn/include/emqx_authn.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
@ -341,12 +342,12 @@ t_check_undefined_expire(_Config) ->
?assertMatch( ?assertMatch(
{matched, allow}, {matched, allow},
emqx_authz_client_info:authorize(Client, subscribe, <<"a/b">>, undefined) emqx_authz_client_info:authorize(Client, ?AUTHZ_SUBSCRIBE, <<"a/b">>, undefined)
), ),
?assertMatch( ?assertMatch(
{matched, deny}, {matched, deny},
emqx_authz_client_info:authorize(Client, subscribe, <<"a/bar">>, undefined) emqx_authz_client_info:authorize(Client, ?AUTHZ_SUBSCRIBE, <<"a/bar">>, undefined)
). ).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -18,6 +18,8 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-compile(export_all). -compile(export_all).
-include_lib("emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
@ -44,6 +46,7 @@ init_per_testcase(_TestCase, Config) ->
Config. Config.
end_per_testcase(_TestCase, _Config) -> end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
ok = emqx_authz_mnesia:purge_rules(). ok = emqx_authz_mnesia:purge_rules().
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->
@ -54,51 +57,135 @@ set_special_configs(_) ->
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_username_topic_rules(_Config) ->
ok = test_topic_rules(username).
t_clientid_topic_rules(_Config) -> t_authz(_Config) ->
ok = test_topic_rules(clientid). ClientInfo = emqx_authz_test_lib:base_client_info(),
t_all_topic_rules(_Config) -> test_authz(
ok = test_topic_rules(all). allow,
allow,
{all, #{
<<"permission">> => <<"allow">>, <<"action">> => <<"subscribe">>, <<"topic">> => <<"t">>
}},
{ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t">>}
),
test_authz(
allow,
allow,
{{username, <<"username">>}, #{
<<"permission">> => <<"allow">>,
<<"action">> => <<"subscribe">>,
<<"topic">> => <<"t/${username}">>
}},
{ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t/username">>}
),
test_authz(
allow,
allow,
{{username, <<"username">>}, #{
<<"permission">> => <<"allow">>,
<<"action">> => <<"subscribe">>,
<<"topic">> => <<"eq t/${username}">>
}},
{ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t/${username}">>}
),
test_authz(
deny,
deny,
{{username, <<"username">>}, #{
<<"permission">> => <<"allow">>,
<<"action">> => <<"subscribe">>,
<<"topic">> => <<"eq t/${username}">>
}},
{ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t/username">>}
),
test_authz(
allow,
allow,
{{clientid, <<"clientid">>}, #{
<<"permission">> => <<"allow">>,
<<"action">> => <<"subscribe">>,
<<"topic">> => <<"eq t/${username}">>
}},
{ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t/${username}">>}
),
test_authz(
allow,
allow,
{
{clientid, <<"clientid">>},
#{
<<"permission">> => <<"allow">>,
<<"action">> => <<"publish">>,
<<"topic">> => <<"t">>,
<<"qos">> => <<"1,2">>,
<<"retain">> => <<"true">>
}
},
{ClientInfo, ?AUTHZ_PUBLISH(1, true), <<"t">>}
),
test_authz(
deny,
allow,
{
{clientid, <<"clientid">>},
#{
<<"permission">> => <<"allow">>,
<<"action">> => <<"publish">>,
<<"topic">> => <<"t">>,
<<"qos">> => <<"1,2">>,
<<"retain">> => <<"true">>
}
},
{ClientInfo, ?AUTHZ_PUBLISH(0, true), <<"t">>}
),
test_authz(
deny,
allow,
{
{clientid, <<"clientid">>},
#{
<<"permission">> => <<"allow">>,
<<"action">> => <<"publish">>,
<<"topic">> => <<"t">>,
<<"qos">> => <<"1,2">>,
<<"retain">> => <<"true">>
}
},
{ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t">>}
).
test_topic_rules(Key) -> test_authz(Expected, ExpectedNoRichActions, {Who, Rule}, {ClientInfo, Action, Topic}) ->
ClientInfo = #{ test_authz_with_rich_actions(true, Expected, {Who, Rule}, {ClientInfo, Action, Topic}),
clientid => <<"clientid">>, test_authz_with_rich_actions(
username => <<"username">>, false, ExpectedNoRichActions, {Who, Rule}, {ClientInfo, Action, Topic}
peerhost => {127, 0, 0, 1}, ).
zone => default,
listener => {tcp, default}
},
SetupSamples = fun(CInfo, Samples) -> test_authz_with_rich_actions(
setup_client_samples(CInfo, Samples, Key) RichActionsEnabled, Expected, {Who, Rule}, {ClientInfo, Action, Topic}
end, ) ->
ct:pal("Test authz rich_actions:~p~nwho:~p~nrule:~p~nattempt:~p~nexpected ~p", [
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, SetupSamples), RichActionsEnabled, Who, Rule, {ClientInfo, Action, Topic}, Expected
]),
ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, SetupSamples), try
_ = emqx_authz:set_feature_available(rich_actions, RichActionsEnabled),
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, SetupSamples). ok = emqx_authz_mnesia:store_rules(Who, [Rule]),
?assertEqual(Expected, emqx_access_control:authorize(ClientInfo, Action, Topic))
after
ok = emqx_authz_mnesia:purge_rules()
end.
t_normalize_rules(_Config) -> t_normalize_rules(_Config) ->
ClientInfo = #{ ClientInfo = emqx_authz_test_lib:base_client_info(),
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_mnesia:store_rules( ok = emqx_authz_mnesia:store_rules(
{username, <<"username">>}, {username, <<"username">>},
[{allow, publish, "t"}] [#{<<"permission">> => <<"allow">>, <<"action">> => <<"publish">>, <<"topic">> => <<"t">>}]
), ),
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
), ),
?assertException( ?assertException(
@ -106,25 +193,31 @@ t_normalize_rules(_Config) ->
{invalid_rule, _}, {invalid_rule, _},
emqx_authz_mnesia:store_rules( emqx_authz_mnesia:store_rules(
{username, <<"username">>}, {username, <<"username">>},
[[allow, publish, <<"t">>]] [[<<"allow">>, <<"publish">>, <<"t">>]]
) )
), ),
?assertException( ?assertException(
error, error,
{invalid_rule_action, _}, {invalid_action, _},
emqx_authz_mnesia:store_rules( emqx_authz_mnesia:store_rules(
{username, <<"username">>}, {username, <<"username">>},
[{allow, pub, <<"t">>}] [#{<<"permission">> => <<"allow">>, <<"action">> => <<"pub">>, <<"topic">> => <<"t">>}]
) )
), ),
?assertException( ?assertException(
error, error,
{invalid_rule_permission, _}, {invalid_permission, _},
emqx_authz_mnesia:store_rules( emqx_authz_mnesia:store_rules(
{username, <<"username">>}, {username, <<"username">>},
[{accept, publish, <<"t">>}] [
#{
<<"permission">> => <<"accept">>,
<<"action">> => <<"publish">>,
<<"topic">> => <<"t">>
}
]
) )
). ).
@ -138,27 +231,5 @@ raw_mnesia_authz_config() ->
<<"type">> => <<"built_in_database">> <<"type">> => <<"built_in_database">>
}. }.
setup_client_samples(ClientInfo, Samples, Key) ->
ok = emqx_authz_mnesia:purge_rules(),
Rules = lists:flatmap(
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:map(
fun(Topic) ->
{binary_to_atom(Permission), binary_to_atom(Action), Topic}
end,
Topics
)
end,
Samples
),
#{username := Username, clientid := ClientId} = ClientInfo,
Who =
case Key of
username -> {username, Username};
clientid -> {clientid, ClientId};
all -> all
end,
ok = emqx_authz_mnesia:store_rules(Who, Rules).
setup_config() -> setup_config() ->
emqx_authz_test_lib:setup_config(raw_mnesia_authz_config(), #{}). emqx_authz_test_lib:setup_config(raw_mnesia_authz_config(), #{}).

View File

@ -28,10 +28,10 @@
-define(MONGO_CLIENT, 'emqx_authz_mongo_SUITE_client'). -define(MONGO_CLIENT, 'emqx_authz_mongo_SUITE_client').
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()).
groups() -> groups() ->
[]. emqx_authz_test_lib:table_groups(t_run_case, cases()).
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
@ -57,12 +57,18 @@ set_special_configs(emqx_authz) ->
set_special_configs(_) -> set_special_configs(_) ->
ok. ok.
init_per_group(Group, Config) ->
[{test_case, emqx_authz_test_lib:get_case(Group, cases())} | Config].
end_per_group(_Group, _Config) ->
ok.
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
{ok, _} = mc_worker_api:connect(mongo_config()), {ok, _} = mc_worker_api:connect(mongo_config()),
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),
Config. Config.
end_per_testcase(_TestCase, _Config) -> end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
ok = reset_samples(), ok = reset_samples(),
ok = mc_worker_api:disconnect(?MONGO_CLIENT). ok = mc_worker_api:disconnect(?MONGO_CLIENT).
@ -70,233 +76,313 @@ end_per_testcase(_TestCase, _Config) ->
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_topic_rules(_Config) -> t_run_case(Config) ->
ClientInfo = #{ Case = ?config(test_case, Config),
clientid => <<"clientid">>, ok = setup_source_data(Case),
username => <<"username">>, ok = setup_authz_source(Case),
peerhost => {127, 0, 0, 1}, ok = emqx_authz_test_lib:run_checks(Case).
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2), %%------------------------------------------------------------------------------
%% Cases
%%------------------------------------------------------------------------------
ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, fun setup_client_samples/2), cases() ->
[
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
t_complex_filter(_) ->
%% atom and string values also supported
ClientInfo = #{
clientid => clientid,
username => "username",
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
Samples = [
#{ #{
<<"x">> => #{ name => base_publish,
<<"u">> => <<"username">>, records => [
<<"c">> => [#{<<"c">> => <<"clientid">>}], #{
<<"y">> => 1 <<"username">> => <<"username">>,
}, <<"action">> => <<"publish">>,
<<"permission">> => <<"allow">>, <<"topic">> => <<"a">>,
<<"action">> => <<"publish">>, <<"permission">> => <<"allow">>
<<"topics">> => [<<"t">>] },
} #{
], <<"username">> => <<"username">>,
<<"action">> => <<"subscribe">>,
ok = setup_samples(Samples), <<"topic">> => <<"b">>,
ok = setup_config( <<"permission">> => <<"allow">>
#{ },
<<"filter">> => #{ #{
<<"x">> => #{ <<"username">> => <<"username">>,
<<"u">> => <<"${username}">>, <<"action">> => <<"all">>,
<<"c">> => [#{<<"c">> => <<"${clientid}">>}], <<"topics">> => [<<"c">>, <<"d">>],
<<"y">> => 1 <<"permission">> => <<"allow">>
} }
} ],
filter => #{<<"username">> => <<"${username}">>},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"a">>},
{deny, ?AUTHZ_PUBLISH, <<"b">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"b">>},
{allow, ?AUTHZ_PUBLISH, <<"c">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"c">>},
{allow, ?AUTHZ_PUBLISH, <<"d">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"d">>}
]
},
#{
name => filter_works,
records => [
#{
<<"action">> => <<"publish">>,
<<"topic">> => <<"a">>,
<<"permission">> => <<"allow">>
}
],
filter => #{<<"username">> => <<"${username}">>},
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => invalid_rich_rules,
features => [rich_actions],
records => [
#{
<<"action">> => <<"publish">>,
<<"topic">> => <<"a">>,
<<"permission">> => <<"allow">>,
<<"qos">> => <<"1,2,3">>
},
#{
<<"action">> => <<"publish">>,
<<"topic">> => <<"a">>,
<<"permission">> => <<"allow">>,
<<"retain">> => <<"yes">>
}
],
filter => #{},
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => invalid_rules,
records => [
#{
<<"action">> => <<"publis">>,
<<"topic">> => <<"a">>,
<<"permission">> => <<"allow">>
}
],
filter => #{},
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => rule_by_clientid_cn_dn_peerhost,
records => [
#{
<<"cn">> => <<"cn">>,
<<"dn">> => <<"dn">>,
<<"clientid">> => <<"clientid">>,
<<"peerhost">> => <<"127.0.0.1">>,
<<"action">> => <<"publish">>,
<<"topic">> => <<"a">>,
<<"permission">> => <<"allow">>
}
],
client_info => #{
cn => <<"cn">>,
dn => <<"dn">>
},
filter => #{
<<"cn">> => <<"${cert_common_name}">>,
<<"dn">> => <<"${cert_subject}">>,
<<"clientid">> => <<"${clientid}">>,
<<"peerhost">> => <<"${peerhost}">>
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => topics_literal_wildcard_variable,
records => [
#{
<<"username">> => <<"username">>,
<<"action">> => <<"publish">>,
<<"permission">> => <<"allow">>,
<<"topics">> => [
<<"t/${username}">>,
<<"t/${clientid}">>,
<<"t1/#">>,
<<"t2/+">>,
<<"eq t3/${username}">>
]
}
],
filter => #{<<"username">> => <<"${username}">>},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"t/username">>},
{allow, ?AUTHZ_PUBLISH, <<"t/clientid">>},
{allow, ?AUTHZ_PUBLISH, <<"t1/a/b">>},
{allow, ?AUTHZ_PUBLISH, <<"t2/a">>},
{allow, ?AUTHZ_PUBLISH, <<"t3/${username}">>},
{deny, ?AUTHZ_PUBLISH, <<"t3/username">>}
]
},
#{
name => qos_retain_in_query_result,
features => [rich_actions],
records => [
#{
<<"username">> => <<"username">>,
<<"action">> => <<"publish">>,
<<"permission">> => <<"allow">>,
<<"topic">> => <<"a">>,
<<"qos">> => 1,
<<"retain">> => true
},
#{
<<"username">> => <<"username">>,
<<"action">> => <<"publish">>,
<<"permission">> => <<"allow">>,
<<"topic">> => <<"b">>,
<<"qos">> => <<"1">>,
<<"retain">> => <<"true">>
},
#{
<<"username">> => <<"username">>,
<<"action">> => <<"publish">>,
<<"permission">> => <<"allow">>,
<<"topic">> => <<"c">>,
<<"qos">> => <<"1,2">>,
<<"retain">> => 1
},
#{
<<"username">> => <<"username">>,
<<"action">> => <<"publish">>,
<<"permission">> => <<"allow">>,
<<"topic">> => <<"d">>,
<<"qos">> => [1, 2],
<<"retain">> => <<"1">>
},
#{
<<"username">> => <<"username">>,
<<"action">> => <<"publish">>,
<<"permission">> => <<"allow">>,
<<"topic">> => <<"e">>,
<<"qos">> => [1, 2],
<<"retain">> => <<"all">>
},
#{
<<"username">> => <<"username">>,
<<"action">> => <<"publish">>,
<<"permission">> => <<"allow">>,
<<"topic">> => <<"f">>,
<<"qos">> => null,
<<"retain">> => null
}
],
filter => #{<<"username">> => <<"${username}">>},
checks => [
{allow, ?AUTHZ_PUBLISH(1, true), <<"a">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"a">>},
{allow, ?AUTHZ_PUBLISH(1, true), <<"b">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"b">>},
{deny, ?AUTHZ_PUBLISH(2, false), <<"b">>},
{allow, ?AUTHZ_PUBLISH(2, true), <<"c">>},
{deny, ?AUTHZ_PUBLISH(2, false), <<"c">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"c">>},
{allow, ?AUTHZ_PUBLISH(2, true), <<"d">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"d">>},
{allow, ?AUTHZ_PUBLISH(1, false), <<"e">>},
{allow, ?AUTHZ_PUBLISH(1, true), <<"e">>},
{deny, ?AUTHZ_PUBLISH(0, false), <<"e">>},
{allow, ?AUTHZ_PUBLISH, <<"f">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"f">>}
]
},
#{
name => nonbin_values_in_client_info,
records => [
#{
<<"username">> => <<"username">>,
<<"clientid">> => <<"clientid">>,
<<"action">> => <<"publish">>,
<<"topic">> => <<"a">>,
<<"permission">> => <<"allow">>
}
],
client_info => #{
username => "username",
clientid => clientid
},
filter => #{<<"username">> => <<"${username}">>, <<"clientid">> => <<"${clientid}">>},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => invalid_query,
records => [
#{
<<"action">> => <<"publish">>,
<<"topic">> => <<"a">>,
<<"permission">> => <<"allow">>
}
],
filter => #{<<"$in">> => #{<<"a">> => 1}},
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => complex_query,
records => [
#{
<<"a">> => #{<<"u">> => <<"clientid">>, <<"c">> => [<<"cn">>, <<"dn">>]},
<<"action">> => <<"publish">>,
<<"topic">> => <<"a">>,
<<"permission">> => <<"allow">>
}
],
client_info => #{
cn => <<"cn">>,
dn => <<"dn">>
},
filter => #{
<<"a">> => #{
<<"u">> => <<"${clientid}">>,
<<"c">> => [<<"${cert_common_name}">>, <<"${cert_subject}">>]
}
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>}
]
} }
), ].
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{allow, publish, <<"t">>}]
).
t_mongo_error(_Config) ->
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_samples([]),
ok = setup_config(
#{<<"filter">> => #{<<"$badoperator">> => <<"$badoperator">>}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{deny, publish, <<"t">>}]
).
t_lookups(_Config) ->
ClientInfo = #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ByClientid = #{
<<"clientid">> => <<"clientid">>,
<<"topics">> => [<<"a">>],
<<"action">> => <<"all">>,
<<"permission">> => <<"allow">>
},
ok = setup_samples([ByClientid]),
ok = setup_config(
#{<<"filter">> => #{<<"clientid">> => <<"${clientid}">>}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByPeerhost = #{
<<"peerhost">> => <<"127.0.0.1">>,
<<"topics">> => [<<"a">>],
<<"action">> => <<"all">>,
<<"permission">> => <<"allow">>
},
ok = setup_samples([ByPeerhost]),
ok = setup_config(
#{<<"filter">> => #{<<"peerhost">> => <<"${peerhost}">>}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByCN = #{
<<"CN">> => <<"cn">>,
<<"topics">> => [<<"a">>],
<<"action">> => <<"all">>,
<<"permission">> => <<"allow">>
},
ok = setup_samples([ByCN]),
ok = setup_config(
#{<<"filter">> => #{<<"CN">> => ?PH_CERT_CN_NAME}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByDN = #{
<<"DN">> => <<"dn">>,
<<"topics">> => [<<"a">>],
<<"action">> => <<"all">>,
<<"permission">> => <<"allow">>
},
ok = setup_samples([ByDN]),
ok = setup_config(
#{<<"filter">> => #{<<"DN">> => ?PH_CERT_SUBJECT}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
t_bad_filter(_Config) ->
ClientInfo = #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_config(
#{<<"filter">> => #{<<"$in">> => #{<<"a">> => 1}}}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{deny, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
populate_records(AclRecords, AdditionalData) ->
[maps:merge(Record, AdditionalData) || Record <- AclRecords].
setup_samples(AclRecords) ->
ok = reset_samples(),
{{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"acl">>, AclRecords),
ok.
setup_client_samples(ClientInfo, Samples) ->
#{username := Username} = ClientInfo,
Records = lists:map(
fun(Sample) ->
#{
topics := Topics,
permission := Permission,
action := Action
} = Sample,
#{
<<"topics">> => Topics,
<<"permission">> => Permission,
<<"action">> => Action,
<<"username">> => Username
}
end,
Samples
),
setup_samples(Records),
setup_config(#{<<"filter">> => #{<<"username">> => <<"${username}">>}}).
reset_samples() -> reset_samples() ->
{true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"acl">>, #{}), {true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"acl">>, #{}),
ok. ok.
setup_source_data(#{records := Records}) ->
{{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"acl">>, Records),
ok.
setup_authz_source(#{filter := Filter}) ->
setup_config(
#{
<<"filter">> => Filter
}
).
setup_config(SpecialParams) -> setup_config(SpecialParams) ->
emqx_authz_test_lib:setup_config( emqx_authz_test_lib:setup_config(
raw_mongo_authz_config(), raw_mongo_authz_config(),

View File

@ -27,10 +27,10 @@
-define(MYSQL_RESOURCE, <<"emqx_authz_mysql_SUITE">>). -define(MYSQL_RESOURCE, <<"emqx_authz_mysql_SUITE">>).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()).
groups() -> groups() ->
[]. emqx_authz_test_lib:table_groups(t_run_case, cases()).
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
@ -41,13 +41,7 @@ init_per_suite(Config) ->
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( ok = create_mysql_resource(),
?MYSQL_RESOURCE,
?RESOURCE_GROUP,
emqx_mysql,
mysql_config(),
#{}
),
Config; Config;
false -> false ->
{skip, no_mysql} {skip, no_mysql}
@ -59,9 +53,18 @@ end_per_suite(_Config) ->
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_group(Group, Config) ->
[{test_case, emqx_authz_test_lib:get_case(Group, cases())} | Config].
end_per_group(_Group, _Config) ->
ok.
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),
Config. Config.
end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
ok = drop_table(),
ok.
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers(); ok = emqx_authz_test_lib:reset_authorizers();
@ -72,189 +75,11 @@ set_special_configs(_) ->
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_topic_rules(_Config) -> t_run_case(Config) ->
ClientInfo = #{ Case = ?config(test_case, Config),
clientid => <<"clientid">>, ok = setup_source_data(Case),
username => <<"username">>, ok = setup_authz_source(Case),
peerhost => {127, 0, 0, 1}, ok = emqx_authz_test_lib:run_checks(Case).
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, fun setup_client_samples/2),
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
t_lookups(_Config) ->
ClientInfo = #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
%% by clientid
ok = init_table(),
ok = q(
<<
"INSERT INTO acl(clientid, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE clientid = ${clientid}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by peerhost
ok = init_table(),
ok = q(
<<
"INSERT INTO acl(peerhost, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE peerhost = ${peerhost}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by cn
ok = init_table(),
ok = q(
<<
"INSERT INTO acl(cn, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE cn = ${cert_common_name}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by dn
ok = init_table(),
ok = q(
<<
"INSERT INTO acl(dn, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE dn = ${cert_subject}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% strip double quote support
ok = init_table(),
ok = q(
<<
"INSERT INTO acl(clientid, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE clientid = \"${clientid}\""
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
t_mysql_error(_Config) ->
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_config(
#{<<"query">> => <<"SOME INVALID STATEMENT">>}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{deny, subscribe, <<"a">>}]
).
t_create_invalid(_Config) -> t_create_invalid(_Config) ->
BadConfig = maps:merge( BadConfig = maps:merge(
@ -265,45 +90,285 @@ t_create_invalid(_Config) ->
[_] = emqx_authz:lookup(). [_] = emqx_authz:lookup().
t_nonbinary_values(_Config) -> %%------------------------------------------------------------------------------
ClientInfo = #{ %% Cases
clientid => clientid, %%------------------------------------------------------------------------------
username => "username",
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = init_table(), cases() ->
ok = q( [
<<
"INSERT INTO acl(clientid, username, topic, permission, action)"
"VALUES(?, ?, ?, ?, ?)"
>>,
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{ #{
<<"query">> => << name => base_publish,
"SELECT permission, action, topic " setup => [
"FROM acl WHERE clientid = ${clientid} AND username = ${username}" "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
>> "permission VARCHAR(255), action VARCHAR(255))",
} "INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'allow', 'publish')",
), "INSERT INTO acl(username, topic, permission, action) VALUES('username', 'b', 'allow', 'subscribe')"
],
query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
client_info => #{username => <<"username">>},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>},
{deny, ?AUTHZ_PUBLISH, <<"b">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"a">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"b">>}
]
},
#{
name => rule_by_clientid_cn_dn_peerhost,
setup => [
"CREATE TABLE acl(clientid VARCHAR(255), cn VARCHAR(255), dn VARCHAR(255),"
" peerhost VARCHAR(255), topic VARCHAR(255), permission VARCHAR(255), action VARCHAR(255))",
ok = emqx_authz_test_lib:test_samples( "INSERT INTO acl(clientid, cn, dn, peerhost, topic, permission, action)"
ClientInfo, " VALUES('clientid', 'cn', 'dn', '127.0.0.1', 'a', 'allow', 'publish')"
[ ],
{allow, subscribe, <<"a">>}, query =>
{deny, subscribe, <<"b">>} "SELECT permission, action, topic FROM acl WHERE"
] " clientid = ${clientid} AND cn = ${cert_common_name}"
). " AND dn = ${cert_subject} AND peerhost = ${peerhost}",
client_info => #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
peerhost => {127, 0, 0, 1}
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>},
{deny, ?AUTHZ_PUBLISH, <<"b">>}
]
},
#{
name => topics_literal_wildcard_variable,
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255))",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 't/${username}', 'allow', 'publish')",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 't/${clientid}', 'allow', 'publish')",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 'eq t/${username}', 'allow', 'publish')",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 't/#', 'allow', 'publish')",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 't1/+', 'allow', 'publish')"
],
query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
client_info => #{
username => <<"username">>
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"t/username">>},
{allow, ?AUTHZ_PUBLISH, <<"t/clientid">>},
{allow, ?AUTHZ_PUBLISH, <<"t/${username}">>},
{allow, ?AUTHZ_PUBLISH, <<"t/1/2">>},
{allow, ?AUTHZ_PUBLISH, <<"t1/1">>},
{deny, ?AUTHZ_PUBLISH, <<"t1/1/2">>},
{deny, ?AUTHZ_PUBLISH, <<"abc">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"t/username">>}
]
},
#{
name => qos_retain_in_query_result,
features => [rich_actions],
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255),"
"qos_s VARCHAR(255), retain_s VARCHAR(255))",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't1', 'allow', 'publish', '1', 'true')",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't2', 'allow', 'publish', '2', 'false')",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't3', 'allow', 'publish', '0,1,2', 'all')",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't4', 'allow', 'subscribe', '1', null)",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't5', 'allow', 'subscribe', '0,1,2', null)"
],
query =>
"SELECT permission, action, topic, qos_s as qos, retain_s as retain"
" FROM acl WHERE username = ${username}",
client_info => #{
username => <<"username">>
},
checks => [
{allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"t1">>},
{allow, ?AUTHZ_PUBLISH(2, false), <<"t2">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"t2">>},
{deny, ?AUTHZ_PUBLISH(2, true), <<"t2">>},
{allow, ?AUTHZ_PUBLISH(1, true), <<"t3">>},
{allow, ?AUTHZ_PUBLISH(2, false), <<"t3">>},
{allow, ?AUTHZ_PUBLISH(2, true), <<"t3">>},
{allow, ?AUTHZ_PUBLISH(0, false), <<"t3">>},
{allow, ?AUTHZ_SUBSCRIBE(1), <<"t4">>},
{deny, ?AUTHZ_SUBSCRIBE(2), <<"t4">>},
{allow, ?AUTHZ_SUBSCRIBE(1), <<"t5">>},
{allow, ?AUTHZ_SUBSCRIBE(2), <<"t5">>},
{allow, ?AUTHZ_SUBSCRIBE(0), <<"t5">>}
]
},
#{
name => qos_retain_in_query_result_as_integer,
features => [rich_actions],
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255),"
"qos_i VARCHAR(255), retain_i VARCHAR(255))",
"INSERT INTO acl(username, topic, permission, action, qos_i, retain_i)"
" VALUES('username', 't1', 'allow', 'publish', 1, 1)"
],
query =>
"SELECT permission, action, topic, qos_i as qos, retain_i as retain"
" FROM acl WHERE username = ${username}",
client_info => #{
username => <<"username">>
},
checks => [
{allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"t1">>}
]
},
#{
name => retain_in_query_result_as_boolean,
features => [rich_actions],
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), permission VARCHAR(255),"
" action VARCHAR(255), retain_b BOOLEAN)",
"INSERT INTO acl(username, topic, permission, action, retain_b)"
" VALUES('username', 't1', 'allow', 'publish', true)",
"INSERT INTO acl(username, topic, permission, action, retain_b)"
" VALUES('username', 't2', 'allow', 'publish', false)"
],
query =>
"SELECT permission, action, topic, retain_b as retain"
" FROM acl WHERE username = ${username}",
client_info => #{
username => <<"username">>
},
checks => [
{allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
{allow, ?AUTHZ_PUBLISH(1, false), <<"t2">>},
{deny, ?AUTHZ_PUBLISH(1, true), <<"t2">>}
]
},
#{
name => nonbin_values_in_client_info,
setup => [
"CREATE TABLE acl(who VARCHAR(255), topic VARCHAR(255), permission VARCHAR(255),"
" action VARCHAR(255))",
"INSERT INTO acl(who, topic, permission, action)"
" VALUES('username', 't/${username}', 'allow', 'publish')",
"INSERT INTO acl(who, topic, permission, action)"
" VALUES('clientid', 't/${clientid}', 'allow', 'publish')"
],
query =>
"SELECT permission, action, topic"
" FROM acl WHERE who = ${username} OR who = ${clientid}",
client_info => #{
%% string, not a binary
username => "username",
%% atom, not a binary
clientid => clientid
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"t/username">>},
{allow, ?AUTHZ_PUBLISH, <<"t/clientid">>},
{deny, ?AUTHZ_PUBLISH, <<"t/foo">>}
]
},
#{
name => null_retain_qos,
features => [rich_actions],
setup => [
"CREATE TABLE acl(qos VARCHAR(255), retain VARCHAR(255),"
" topic VARCHAR(255), permission VARCHAR(255), action VARCHAR(255))",
"INSERT INTO acl(qos, retain, topic, permission, action)"
" VALUES(NULL, NULL, 'tp', 'allow', 'publish')"
],
query =>
"SELECT permission, action, topic, qos FROM acl",
checks => [
{allow, ?AUTHZ_PUBLISH(0, false), <<"tp">>},
{allow, ?AUTHZ_PUBLISH(1, false), <<"tp">>},
{allow, ?AUTHZ_PUBLISH(2, true), <<"tp">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"xxx">>}
]
},
#{
name => strip_double_quote,
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255))",
"INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'allow', 'publish')"
],
query => "SELECT permission, action, topic FROM acl WHERE username = \"${username}\"",
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => invalid_query,
setup => [],
query => "SELECT permission, action, topic FROM acl WHER",
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => pgsql_error,
setup => [],
query =>
"SELECT permission, action, topic FROM table_not_exists WHERE username = ${username}",
checks => [
{deny, ?AUTHZ_PUBLISH, <<"t">>}
]
}
].
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
setup_source_data(#{setup := Queries}) ->
lists:foreach(
fun(Query) ->
_ = q(Query)
end,
Queries
).
setup_authz_source(#{query := Query}) ->
setup_config(
#{
<<"query">> => Query
}
).
raw_mysql_authz_config() -> raw_mysql_authz_config() ->
#{ #{
<<"enable">> => <<"true">>, <<"enable">> => <<"true">>,
@ -332,52 +397,9 @@ q(Sql, Params) ->
{sql, Sql, Params} {sql, Sql, Params}
). ).
init_table() ->
ok = drop_table(),
ok = q(
"CREATE TABLE acl(\n"
" username VARCHAR(255),\n"
" clientid VARCHAR(255),\n"
" peerhost VARCHAR(255),\n"
" cn VARCHAR(255),\n"
" dn VARCHAR(255),\n"
" topic VARCHAR(255),\n"
" permission VARCHAR(255),\n"
" action VARCHAR(255))"
).
drop_table() -> drop_table() ->
ok = q("DROP TABLE IF EXISTS acl"). ok = q("DROP TABLE IF EXISTS acl").
setup_client_samples(ClientInfo, Samples) ->
#{username := Username} = ClientInfo,
ok = init_table(),
ok = lists:foreach(
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:foreach(
fun(Topic) ->
q(
<<
"INSERT INTO acl(username, topic, permission, action)"
"VALUES(?, ?, ?, ?)"
>>,
[Username, Topic, Permission, Action]
)
end,
Topics
)
end,
Samples
),
setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE username = ${username}"
>>
}
).
setup_config(SpecialParams) -> setup_config(SpecialParams) ->
emqx_authz_test_lib:setup_config( emqx_authz_test_lib:setup_config(
raw_mysql_authz_config(), raw_mysql_authz_config(),
@ -400,3 +422,13 @@ start_apps(Apps) ->
stop_apps(Apps) -> stop_apps(Apps) ->
lists:foreach(fun application:stop/1, Apps). lists:foreach(fun application:stop/1, Apps).
create_mysql_resource() ->
{ok, _} = emqx_resource:create_local(
?MYSQL_RESOURCE,
?RESOURCE_GROUP,
emqx_mysql,
mysql_config(),
#{}
),
ok.

View File

@ -27,10 +27,10 @@
-define(PGSQL_RESOURCE, <<"emqx_authz_pgsql_SUITE">>). -define(PGSQL_RESOURCE, <<"emqx_authz_pgsql_SUITE">>).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()).
groups() -> groups() ->
[]. emqx_authz_test_lib:table_groups(t_run_case, cases()).
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
@ -41,13 +41,7 @@ init_per_suite(Config) ->
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( {ok, _} = create_pgsql_resource(),
?PGSQL_RESOURCE,
?RESOURCE_GROUP,
emqx_connector_pgsql,
pgsql_config(),
#{}
),
Config; Config;
false -> false ->
{skip, no_pgsql} {skip, no_pgsql}
@ -59,9 +53,18 @@ end_per_suite(_Config) ->
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_group(Group, Config) ->
[{test_case, emqx_authz_test_lib:get_case(Group, cases())} | Config].
end_per_group(_Group, _Config) ->
ok.
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),
Config. Config.
end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
ok = drop_table(),
ok.
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers(); ok = emqx_authz_test_lib:reset_authorizers();
@ -72,194 +75,11 @@ set_special_configs(_) ->
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_topic_rules(_Config) -> t_run_case(Config) ->
ClientInfo = #{ Case = ?config(test_case, Config),
clientid => <<"clientid">>, ok = setup_source_data(Case),
username => <<"username">>, ok = setup_authz_source(Case),
peerhost => {127, 0, 0, 1}, ok = emqx_authz_test_lib:run_checks(Case).
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, fun setup_client_samples/2),
ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, fun setup_client_samples/2).
t_lookups(_Config) ->
ClientInfo = #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
%% by clientid
ok = init_table(),
ok = insert(
<<
"INSERT INTO acl(clientid, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE clientid = ${clientid}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by peerhost
ok = init_table(),
ok = insert(
<<
"INSERT INTO acl(peerhost, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"127.0.0.1">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE peerhost = ${peerhost}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by cn
ok = init_table(),
ok = insert(
<<
"INSERT INTO acl(cn, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"cn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE cn = ${cert_common_name}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% by dn
ok = init_table(),
ok = insert(
<<
"INSERT INTO acl(dn, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"dn">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE dn = ${cert_subject}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
%% strip double quote support
ok = init_table(),
ok = insert(
<<
"INSERT INTO acl(clientid, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[<<"clientid">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE clientid = \"${clientid}\""
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
t_pgsql_error(_Config) ->
ClientInfo = #{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE clientid = ${username}"
>>
}
),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[{deny, subscribe, <<"a">>}]
).
t_create_invalid(_Config) -> t_create_invalid(_Config) ->
BadConfig = maps:merge( BadConfig = maps:merge(
@ -270,45 +90,291 @@ t_create_invalid(_Config) ->
[_] = emqx_authz:lookup(). [_] = emqx_authz:lookup().
t_nonbinary_values(_Config) -> %%------------------------------------------------------------------------------
ClientInfo = #{ %% Cases
clientid => clientid, %%------------------------------------------------------------------------------
username => "username",
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ok = init_table(), cases() ->
ok = insert( [
<<
"INSERT INTO acl(clientid, username, topic, permission, action)"
"VALUES($1, $2, $3, $4, $5)"
>>,
[<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>]
),
ok = setup_config(
#{ #{
<<"query">> => << name => base_publish,
"SELECT permission, action, topic " setup => [
"FROM acl WHERE clientid = ${clientid} AND username = ${username}" "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
>> "permission VARCHAR(255), action VARCHAR(255))",
} "INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'allow', 'publish')",
), "INSERT INTO acl(username, topic, permission, action) VALUES('username', 'b', 'allow', 'subscribe')"
],
query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
client_info => #{username => <<"username">>},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>},
{deny, ?AUTHZ_PUBLISH, <<"b">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"a">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"b">>}
]
},
#{
name => rule_by_clientid_cn_dn_peerhost,
setup => [
"CREATE TABLE acl(clientid VARCHAR(255), cn VARCHAR(255), dn VARCHAR(255),"
" peerhost VARCHAR(255), topic VARCHAR(255),"
" permission VARCHAR(255), action VARCHAR(255))",
ok = emqx_authz_test_lib:test_samples( "INSERT INTO acl(clientid, cn, dn, peerhost, topic, permission, action)"
ClientInfo, " VALUES('clientid', 'cn', 'dn', '127.0.0.1', 'a', 'allow', 'publish')"
[ ],
{allow, subscribe, <<"a">>}, query =>
{deny, subscribe, <<"b">>} "SELECT permission, action, topic FROM acl WHERE"
] " clientid = ${clientid} AND cn = ${cert_common_name}"
). " AND dn = ${cert_subject} AND peerhost = ${peerhost}",
client_info => #{
clientid => <<"clientid">>,
cn => <<"cn">>,
dn => <<"dn">>,
peerhost => {127, 0, 0, 1}
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>},
{deny, ?AUTHZ_PUBLISH, <<"b">>}
]
},
#{
name => topics_literal_wildcard_variable,
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255))",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 't/${username}', 'allow', 'publish')",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 't/${clientid}', 'allow', 'publish')",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 'eq t/${username}', 'allow', 'publish')",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 't/#', 'allow', 'publish')",
"INSERT INTO acl(username, topic, permission, action) "
"VALUES('username', 't1/+', 'allow', 'publish')"
],
query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
client_info => #{
username => <<"username">>
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"t/username">>},
{allow, ?AUTHZ_PUBLISH, <<"t/clientid">>},
{allow, ?AUTHZ_PUBLISH, <<"t/${username}">>},
{allow, ?AUTHZ_PUBLISH, <<"t/1/2">>},
{allow, ?AUTHZ_PUBLISH, <<"t1/1">>},
{deny, ?AUTHZ_PUBLISH, <<"t1/1/2">>},
{deny, ?AUTHZ_PUBLISH, <<"abc">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"t/username">>}
]
},
#{
name => qos_retain_in_query_result,
features => [rich_actions],
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255),"
"qos_s VARCHAR(255), retain_s VARCHAR(255))",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't1', 'allow', 'publish', '1', 'true')",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't2', 'allow', 'publish', '2', 'false')",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't3', 'allow', 'publish', '0,1,2', 'all')",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't4', 'allow', 'subscribe', '1', null)",
"INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
" VALUES('username', 't5', 'allow', 'subscribe', '0,1,2', null)"
],
query =>
"SELECT permission, action, topic, qos_s as qos, retain_s as retain"
" FROM acl WHERE username = ${username}",
client_info => #{
username => <<"username">>
},
checks => [
{allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"t1">>},
{allow, ?AUTHZ_PUBLISH(2, false), <<"t2">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"t2">>},
{deny, ?AUTHZ_PUBLISH(2, true), <<"t2">>},
{allow, ?AUTHZ_PUBLISH(1, true), <<"t3">>},
{allow, ?AUTHZ_PUBLISH(2, false), <<"t3">>},
{allow, ?AUTHZ_PUBLISH(2, true), <<"t3">>},
{allow, ?AUTHZ_PUBLISH(0, false), <<"t3">>},
{allow, ?AUTHZ_SUBSCRIBE(1), <<"t4">>},
{deny, ?AUTHZ_SUBSCRIBE(2), <<"t4">>},
{allow, ?AUTHZ_SUBSCRIBE(1), <<"t5">>},
{allow, ?AUTHZ_SUBSCRIBE(2), <<"t5">>},
{allow, ?AUTHZ_SUBSCRIBE(0), <<"t5">>}
]
},
#{
name => qos_retain_in_query_result_as_integer,
features => [rich_actions],
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255),"
"qos_i VARCHAR(255), retain_i VARCHAR(255))",
"INSERT INTO acl(username, topic, permission, action, qos_i, retain_i)"
" VALUES('username', 't1', 'allow', 'publish', 1, 1)"
],
query =>
"SELECT permission, action, topic, qos_i as qos, retain_i as retain"
" FROM acl WHERE username = ${username}",
client_info => #{
username => <<"username">>
},
checks => [
{allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"t1">>}
]
},
#{
name => retain_in_query_result_as_boolean,
features => [rich_actions],
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), permission VARCHAR(255),"
" action VARCHAR(255), retain_b BOOLEAN)",
"INSERT INTO acl(username, topic, permission, action, retain_b)"
" VALUES('username', 't1', 'allow', 'publish', true)",
"INSERT INTO acl(username, topic, permission, action, retain_b)"
" VALUES('username', 't2', 'allow', 'publish', false)"
],
query =>
"SELECT permission, action, topic, retain_b as retain"
" FROM acl WHERE username = ${username}",
client_info => #{
username => <<"username">>
},
checks => [
{allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
{allow, ?AUTHZ_PUBLISH(1, false), <<"t2">>},
{deny, ?AUTHZ_PUBLISH(1, true), <<"t2">>}
]
},
#{
name => nonbin_values_in_client_info,
setup => [
"CREATE TABLE acl(who VARCHAR(255), topic VARCHAR(255), permission VARCHAR(255),"
" action VARCHAR(255))",
"INSERT INTO acl(who, topic, permission, action)"
" VALUES('username', 't/${username}', 'allow', 'publish')",
"INSERT INTO acl(who, topic, permission, action)"
" VALUES('clientid', 't/${clientid}', 'allow', 'publish')"
],
query =>
"SELECT permission, action, topic"
" FROM acl WHERE who = ${username} OR who = ${clientid}",
client_info => #{
%% string, not a binary
username => "username",
%% atom, not a binary
clientid => clientid
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"t/username">>},
{allow, ?AUTHZ_PUBLISH, <<"t/clientid">>},
{deny, ?AUTHZ_PUBLISH, <<"t/foo">>}
]
},
#{
name => array_null_qos,
features => [rich_actions],
setup => [
"CREATE TABLE acl(qos INTEGER[], "
" topic VARCHAR(255), permission VARCHAR(255), action VARCHAR(255))",
"INSERT INTO acl(qos, topic, permission, action)"
" VALUES('{1,2}', 'tp', 'allow', 'publish')",
"INSERT INTO acl(qos, topic, permission, action)"
" VALUES(NULL, 'ts', 'allow', 'subscribe')"
],
query =>
"SELECT permission, action, topic, qos FROM acl",
checks => [
{allow, ?AUTHZ_PUBLISH(1, false), <<"tp">>},
{allow, ?AUTHZ_PUBLISH(2, false), <<"tp">>},
{deny, ?AUTHZ_PUBLISH(3, false), <<"tp">>},
{allow, ?AUTHZ_SUBSCRIBE(1), <<"ts">>},
{allow, ?AUTHZ_SUBSCRIBE(2), <<"ts">>}
]
},
#{
name => strip_double_quote,
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255))",
"INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'allow', 'publish')"
],
query => "SELECT permission, action, topic FROM acl WHERE username = \"${username}\"",
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => invalid_query,
setup => [],
query => "SELECT permission, action, topic FROM acl WHER",
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => pgsql_error,
setup => [],
query =>
"SELECT permission, action, topic FROM table_not_exists WHERE username = ${username}",
checks => [
{deny, ?AUTHZ_PUBLISH, <<"t">>}
]
}
%% TODO: add case for unknown variables after fixing EMQX-10400
].
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
setup_source_data(#{setup := Queries}) ->
lists:foreach(
fun(Query) ->
_ = q(Query)
end,
Queries
).
setup_authz_source(#{query := Query}) ->
setup_config(
#{
<<"query">> => Query
}
).
raw_pgsql_authz_config() -> raw_pgsql_authz_config() ->
#{ #{
<<"enable">> => <<"true">>, <<"enable">> => <<"true">>,
@ -331,61 +397,10 @@ q(Sql) ->
{query, Sql} {query, Sql}
). ).
insert(Sql, Params) ->
{ok, _} = emqx_resource:simple_sync_query(
?PGSQL_RESOURCE,
{query, Sql, Params}
),
ok.
init_table() ->
ok = drop_table(),
{ok, _, _} = q(
"CREATE TABLE acl(\n"
" username VARCHAR(255),\n"
" clientid VARCHAR(255),\n"
" peerhost VARCHAR(255),\n"
" cn VARCHAR(255),\n"
" dn VARCHAR(255),\n"
" topic VARCHAR(255),\n"
" permission VARCHAR(255),\n"
" action VARCHAR(255))"
),
ok.
drop_table() -> drop_table() ->
{ok, _, _} = q("DROP TABLE IF EXISTS acl"), {ok, _, _} = q("DROP TABLE IF EXISTS acl"),
ok. ok.
setup_client_samples(ClientInfo, Samples) ->
#{username := Username} = ClientInfo,
ok = init_table(),
ok = lists:foreach(
fun(#{topics := Topics, permission := Permission, action := Action}) ->
lists:foreach(
fun(Topic) ->
insert(
<<
"INSERT INTO acl(username, topic, permission, action)"
"VALUES($1, $2, $3, $4)"
>>,
[Username, Topic, Permission, Action]
)
end,
Topics
)
end,
Samples
),
setup_config(
#{
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE username = ${username}"
>>
}
).
setup_config(SpecialParams) -> setup_config(SpecialParams) ->
emqx_authz_test_lib:setup_config( emqx_authz_test_lib:setup_config(
raw_pgsql_authz_config(), raw_pgsql_authz_config(),
@ -403,6 +418,15 @@ pgsql_config() ->
ssl => #{enable => false} ssl => #{enable => false}
}. }.
create_pgsql_resource() ->
emqx_resource:create_local(
?PGSQL_RESOURCE,
?RESOURCE_GROUP,
emqx_connector_pgsql,
pgsql_config(),
#{}
).
start_apps(Apps) -> start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps). lists:foreach(fun application:ensure_all_started/1, Apps).

View File

@ -28,10 +28,10 @@
-define(REDIS_RESOURCE, <<"emqx_authz_redis_SUITE">>). -define(REDIS_RESOURCE, <<"emqx_authz_redis_SUITE">>).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()).
groups() -> groups() ->
[]. emqx_authz_test_lib:table_groups(t_run_case, cases()).
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
@ -42,13 +42,7 @@ init_per_suite(Config) ->
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( ok = create_redis_resource(),
?REDIS_RESOURCE,
?RESOURCE_GROUP,
emqx_redis,
redis_config(),
#{}
),
Config; Config;
false -> false ->
{skip, no_redis} {skip, no_redis}
@ -60,9 +54,18 @@ end_per_suite(_Config) ->
ok = stop_apps([emqx_resource]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
init_per_group(Group, Config) ->
[{test_case, emqx_authz_test_lib:get_case(Group, cases())} | Config].
end_per_group(_Group, _Config) ->
ok.
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
ok = emqx_authz_test_lib:reset_authorizers(), ok = emqx_authz_test_lib:reset_authorizers(),
Config. Config.
end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
_ = cleanup_redis(),
ok.
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->
ok = emqx_authz_test_lib:reset_authorizers(); ok = emqx_authz_test_lib:reset_authorizers();
@ -73,93 +76,11 @@ set_special_configs(_) ->
%% Tests %% Tests
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_topic_rules(_Config) -> t_run_case(Config) ->
ClientInfo = #{ Case = ?config(test_case, Config),
clientid => <<"clientid">>, ok = setup_source_data(Case),
username => <<"username">>, ok = setup_authz_source(Case),
peerhost => {127, 0, 0, 1}, ok = emqx_authz_test_lib:run_checks(Case).
zone => default,
listener => {tcp, default}
},
ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, fun setup_client_samples/2),
ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, fun setup_client_samples/2).
t_lookups(_Config) ->
ClientInfo = #{
clientid => <<"client id">>,
cn => <<"cn">>,
dn => <<"dn">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ByClientid = #{
<<"mqtt_user:client id">> =>
#{<<"a">> => <<"all">>}
},
ok = setup_sample(ByClientid),
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${clientid}">>}),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByPeerhost = #{
<<"mqtt_user:127.0.0.1">> =>
#{<<"a">> => <<"all">>}
},
ok = setup_sample(ByPeerhost),
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${peerhost}">>}),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByCN = #{
<<"mqtt_user:cn">> =>
#{<<"a">> => <<"all">>}
},
ok = setup_sample(ByCN),
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${cert_common_name}">>}),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
),
ByDN = #{
<<"mqtt_user:dn">> =>
#{<<"a">> => <<"all">>}
},
ok = setup_sample(ByDN),
ok = setup_config(#{<<"cmd">> => <<"HGETALL mqtt_user:${cert_subject}">>}),
ok = emqx_authz_test_lib:test_samples(
ClientInfo,
[
{allow, subscribe, <<"a">>},
{deny, subscribe, <<"b">>}
]
).
%% should still succeed to create even if the config will not work, %% should still succeed to create even if the config will not work,
%% because it's not a part of the schema check %% because it's not a part of the schema check
@ -181,7 +102,7 @@ t_create_with_config_values_wont_work(_Config) ->
InvalidConfigs InvalidConfigs
). ).
%% creating without a require field should return error %% creating without a required field should return error
t_create_invalid_config(_Config) -> t_create_invalid_config(_Config) ->
AuthzConfig = raw_redis_authz_config(), AuthzConfig = raw_redis_authz_config(),
C = maps:without([<<"server">>], AuthzConfig), C = maps:without([<<"server">>], AuthzConfig),
@ -196,54 +117,211 @@ t_create_invalid_config(_Config) ->
t_redis_error(_Config) -> t_redis_error(_Config) ->
ok = setup_config(#{<<"cmd">> => <<"INVALID COMMAND">>}), ok = setup_config(#{<<"cmd">> => <<"INVALID COMMAND">>}),
ClientInfo = #{ ClientInfo = emqx_authz_test_lib:base_client_info(),
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
deny = emqx_access_control:authorize(ClientInfo, subscribe, <<"a">>). ?assertEqual(
deny,
emqx_access_control:authorize(ClientInfo, ?AUTHZ_SUBSCRIBE, <<"a">>)
).
%%------------------------------------------------------------------------------
%% Cases
%%------------------------------------------------------------------------------
cases() ->
[
#{
name => base_publish,
setup => [
[
"HMSET",
"acl:username",
"a",
"publish",
"b",
"subscribe",
"d",
"all"
]
],
cmd => "HGETALL acl:${username}",
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"a">>},
{deny, ?AUTHZ_PUBLISH, <<"b">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"b">>},
{allow, ?AUTHZ_PUBLISH, <<"d">>},
{allow, ?AUTHZ_SUBSCRIBE, <<"d">>}
]
},
#{
name => invalid_rule,
setup => [
[
"HMSET",
"acl:username",
"a",
"[]",
"b",
"{invalid:json}",
"c",
"pub",
"d",
emqx_utils_json:encode(#{qos => 1, retain => true})
]
],
cmd => "HGETALL acl:${username}",
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>},
{deny, ?AUTHZ_PUBLISH, <<"b">>},
{deny, ?AUTHZ_PUBLISH, <<"c">>},
{deny, ?AUTHZ_PUBLISH(1, true), <<"d">>}
]
},
#{
name => rule_by_clientid_cn_dn_peerhost,
setup => [
["HMSET", "acl:clientid:cn:dn:127.0.0.1", "a", "publish"]
],
cmd => "HGETALL acl:${clientid}:${cert_common_name}:${cert_subject}:${peerhost}",
client_info => #{
cn => <<"cn">>,
dn => <<"dn">>
},
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => topics_literal_wildcard_variable,
setup => [
[
"HMSET",
"acl:username",
"t/${username}",
"publish",
"t/${clientid}",
"publish",
"t1/#",
"publish",
"t2/+",
"publish",
"eq t3/${username}",
"publish"
]
],
cmd => "HGETALL acl:${username}",
checks => [
{allow, ?AUTHZ_PUBLISH, <<"t/username">>},
{allow, ?AUTHZ_PUBLISH, <<"t/clientid">>},
{allow, ?AUTHZ_PUBLISH, <<"t1/a/b">>},
{allow, ?AUTHZ_PUBLISH, <<"t2/a">>},
{allow, ?AUTHZ_PUBLISH, <<"t3/${username}">>},
{deny, ?AUTHZ_PUBLISH, <<"t3/username">>}
]
},
#{
name => qos_retain_in_query_result,
features => [rich_actions],
setup => [
[
"HMSET",
"acl:username",
"a",
emqx_utils_json:encode(#{action => <<"publish">>, qos => 1, retain => true}),
"b",
emqx_utils_json:encode(#{
action => <<"publish">>, qos => <<"1">>, retain => <<"true">>
}),
"c",
emqx_utils_json:encode(#{action => <<"publish">>, qos => <<"1,2">>, retain => 1}),
"d",
emqx_utils_json:encode(#{
action => <<"publish">>, qos => [1, 2], retain => <<"1">>
}),
"e",
emqx_utils_json:encode(#{
action => <<"publish">>, qos => [1, 2], retain => <<"all">>
}),
"f",
emqx_utils_json:encode(#{action => <<"publish">>, qos => null, retain => null})
]
],
cmd => "HGETALL acl:${username}",
checks => [
{allow, ?AUTHZ_PUBLISH(1, true), <<"a">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"a">>},
{allow, ?AUTHZ_PUBLISH(1, true), <<"b">>},
{deny, ?AUTHZ_PUBLISH(1, false), <<"b">>},
{deny, ?AUTHZ_PUBLISH(2, false), <<"b">>},
{allow, ?AUTHZ_PUBLISH(2, true), <<"c">>},
{deny, ?AUTHZ_PUBLISH(2, false), <<"c">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"c">>},
{allow, ?AUTHZ_PUBLISH(2, true), <<"d">>},
{deny, ?AUTHZ_PUBLISH(0, true), <<"d">>},
{allow, ?AUTHZ_PUBLISH(1, false), <<"e">>},
{allow, ?AUTHZ_PUBLISH(1, true), <<"e">>},
{deny, ?AUTHZ_PUBLISH(0, false), <<"e">>},
{allow, ?AUTHZ_PUBLISH, <<"f">>},
{deny, ?AUTHZ_SUBSCRIBE, <<"f">>}
]
},
#{
name => nonbin_values_in_client_info,
setup => [
[
"HMSET",
"acl:username:clientid",
"a",
"publish"
]
],
client_info => #{
username => "username",
clientid => clientid
},
cmd => "HGETALL acl:${username}:${clientid}",
checks => [
{allow, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => invalid_query,
setup => [
["SET", "acl:username", 1]
],
cmd => "HGETALL acl:${username}",
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>}
]
}
].
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Helpers %% Helpers
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
setup_sample(AuthzData) -> setup_source_data(#{setup := Queries}) ->
{ok, _} = q(["FLUSHDB"]), lists:foreach(
ok = lists:foreach( fun(Query) ->
fun({Key, Values}) -> _ = q(Query)
lists:foreach(
fun({TopicFilter, Action}) ->
q(["HSET", Key, TopicFilter, Action])
end,
maps:to_list(Values)
)
end, end,
maps:to_list(AuthzData) Queries
). ).
setup_client_samples(ClientInfo, Samples) -> setup_authz_source(#{cmd := Cmd}) ->
#{username := Username} = ClientInfo, setup_config(
Key = <<"mqtt_user:", Username/binary>>, #{
lists:foreach( <<"cmd">> => Cmd
fun(Sample) -> }
#{ ).
topics := Topics,
permission := <<"allow">>,
action := Action
} = Sample,
lists:foreach(
fun(Topic) ->
q(["HSET", Key, Topic, Action])
end,
Topics
)
end,
Samples
),
setup_config(#{}).
setup_config(SpecialParams) -> setup_config(SpecialParams) ->
Config = maps:merge(raw_redis_authz_config(), SpecialParams), Config = maps:merge(raw_redis_authz_config(), SpecialParams),
@ -261,6 +339,9 @@ raw_redis_authz_config() ->
<<"server">> => <<?REDIS_HOST>> <<"server">> => <<?REDIS_HOST>>
}. }.
cleanup_redis() ->
q([<<"FLUSHALL">>]).
q(Command) -> q(Command) ->
emqx_resource:simple_sync_query( emqx_resource:simple_sync_query(
?REDIS_RESOURCE, ?REDIS_RESOURCE,
@ -283,3 +364,13 @@ start_apps(Apps) ->
stop_apps(Apps) -> stop_apps(Apps) ->
lists:foreach(fun application:stop/1, Apps). lists:foreach(fun application:stop/1, Apps).
create_redis_resource() ->
{ok, _} = emqx_resource:create_local(
?REDIS_RESOURCE,
?RESOURCE_GROUP,
emqx_redis,
redis_config(),
#{}
),
ok.

View File

@ -18,24 +18,17 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-compile(export_all). -compile(export_all).
-include("emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl").
-define(SOURCE1, {deny, all}). -define(CLIENT_INFO_BASE, #{
-define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}). clientid => <<"test">>,
-define(SOURCE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, [?PH_S_CLIENTID]}). username => <<"test">>,
-define(SOURCE4, {allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}). peerhost => {127, 0, 0, 1},
-define(SOURCE5, zone => default,
{allow, listener => {tcp, default}
{'or', [ }).
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
).
-define(SOURCE6, {allow, {username, "test"}, publish, ["t/foo${username}boo"]}).
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -59,6 +52,12 @@ end_per_suite(_Config) ->
emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]), emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
ok. ok.
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
ok.
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny), {ok, _} = emqx:update_config([authorization, no_match], deny),
@ -68,11 +67,11 @@ set_special_configs(_App) ->
ok. ok.
t_compile(_) -> t_compile(_) ->
?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?SOURCE1)), ?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile({deny, all})),
?assertEqual( ?assertEqual(
{allow, {ipaddr, {{127, 0, 0, 1}, {127, 0, 0, 1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]}, {allow, {ipaddr, {{127, 0, 0, 1}, {127, 0, 0, 1}, 32}}, all, [{eq, ['#']}, {eq, ['+']}]},
emqx_authz_rule:compile(?SOURCE2) emqx_authz_rule:compile({allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]})
), ),
?assertEqual( ?assertEqual(
@ -82,14 +81,18 @@ t_compile(_) ->
{{192, 168, 1, 0}, {192, 168, 1, 255}, 24} {{192, 168, 1, 0}, {192, 168, 1, 255}, 24}
]}, ]},
subscribe, [{pattern, [{var, [<<"clientid">>]}]}]}, subscribe, [{pattern, [{var, [<<"clientid">>]}]}]},
emqx_authz_rule:compile(?SOURCE3) emqx_authz_rule:compile(
{allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, [?PH_S_CLIENTID]}
)
), ),
?assertMatch( ?assertEqual(
{allow, {'and', [{clientid, {eq, <<"test">>}}, {username, {eq, <<"test">>}}]}, publish, [ {allow, {'and', [{clientid, {eq, <<"test">>}}, {username, {eq, <<"test">>}}]}, publish, [
[<<"topic">>, <<"test">>] [<<"topic">>, <<"test">>]
]}, ]},
emqx_authz_rule:compile(?SOURCE4) emqx_authz_rule:compile(
{allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}
)
), ),
?assertMatch( ?assertMatch(
@ -101,240 +104,643 @@ t_compile(_) ->
publish, [ publish, [
{pattern, [{var, [<<"username">>]}]}, {pattern, [{var, [<<"clientid">>]}]} {pattern, [{var, [<<"username">>]}]}, {pattern, [{var, [<<"clientid">>]}]}
]}, ]},
emqx_authz_rule:compile(?SOURCE5) emqx_authz_rule:compile(
{allow,
{'or', [
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
)
), ),
?assertEqual( ?assertEqual(
{allow, {username, {eq, <<"test">>}}, publish, [ {allow, {username, {eq, <<"test">>}}, publish, [
{pattern, [{str, <<"t/foo">>}, {var, [<<"username">>]}, {str, <<"boo">>}]} {pattern, [{str, <<"t/foo">>}, {var, [<<"username">>]}, {str, <<"boo">>}]}
]}, ]},
emqx_authz_rule:compile(?SOURCE6) emqx_authz_rule:compile({allow, {username, "test"}, publish, ["t/foo${username}boo"]})
), ),
?assertEqual(
{allow, {username, {eq, <<"test">>}},
#{action_type => publish, qos => [0, 1, 2], retain => all}, [[<<"topic">>, <<"test">>]]},
emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{retain, all}]}, ["topic/test"]}
)
),
?assertEqual(
{allow, {username, {eq, <<"test">>}}, #{action_type => publish, qos => [1], retain => true},
[
[<<"topic">>, <<"test">>]
]},
emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{qos, 1}, {retain, true}]}, ["topic/test"]}
)
),
?assertEqual(
{allow, {username, {eq, <<"test">>}}, #{action_type => subscribe, qos => [1, 2]}, [
[<<"topic">>, <<"test">>]
]},
emqx_authz_rule:compile(
{allow, {username, "test"}, {subscribe, [{qos, 1}, {qos, 2}]}, ["topic/test"]}
)
),
?assertEqual(
{allow, {username, {eq, <<"test">>}}, #{action_type => subscribe, qos => [1]}, [
[<<"topic">>, <<"test">>]
]},
emqx_authz_rule:compile(
{allow, {username, "test"}, {subscribe, [{qos, 1}]}, ["topic/test"]}
)
),
?assertEqual(
{allow, {username, {eq, <<"test">>}}, #{action_type => all, qos => [2], retain => true}, [
[<<"topic">>, <<"test">>]
]},
emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
)
),
ok. ok.
t_compile_ce(_Config) ->
_ = emqx_authz:set_feature_available(rich_actions, false),
?assertThrow(
{invalid_authorization_action, _},
emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
)
),
?assertEqual(
{allow, {username, {eq, <<"test">>}}, all, [[<<"topic">>, <<"test">>]]},
emqx_authz_rule:compile(
{allow, {username, "test"}, all, ["topic/test"]}
)
).
t_match(_) -> t_match(_) ->
ClientInfo1 = #{ ?assertEqual(
clientid => <<"test">>, {matched, deny},
username => <<"test">>, emqx_authz_rule:match(
peerhost => {127, 0, 0, 1}, client_info(),
zone => default, #{action_type => subscribe, qos => 0},
listener => {tcp, default} <<"#">>,
}, emqx_authz_rule:compile({deny, all})
ClientInfo2 = #{ )
clientid => <<"test">>, ),
username => <<"test">>,
peerhost => {192, 168, 1, 10},
zone => default,
listener => {tcp, default}
},
ClientInfo3 = #{
clientid => <<"test">>,
username => <<"fake">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
ClientInfo4 = #{
clientid => <<"fake">>,
username => <<"test">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
},
?assertEqual( ?assertEqual(
{matched, deny}, {matched, deny},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo1, client_info(#{peerhost => {192, 168, 1, 10}}),
subscribe, #{action_type => subscribe, qos => 0},
<<"#">>,
emqx_authz_rule:compile(?SOURCE1)
)
),
?assertEqual(
{matched, deny},
emqx_authz_rule:match(
ClientInfo2,
subscribe,
<<"+">>, <<"+">>,
emqx_authz_rule:compile(?SOURCE1) emqx_authz_rule:compile({deny, all})
) )
), ),
?assertEqual( ?assertEqual(
{matched, deny}, {matched, deny},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo3, client_info(#{username => <<"fake">>}),
subscribe, #{action_type => subscribe, qos => 0},
<<"topic/test">>, <<"topic/test">>,
emqx_authz_rule:compile(?SOURCE1) emqx_authz_rule:compile({deny, all})
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo1, client_info(),
subscribe, #{action_type => subscribe, qos => 0},
<<"#">>, <<"#">>,
emqx_authz_rule:compile(?SOURCE2) emqx_authz_rule:compile({allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]})
) )
), ),
?assertEqual( ?assertEqual(
nomatch, nomatch,
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo1, client_info(),
subscribe, #{action_type => subscribe, qos => 0},
<<"topic/test">>, <<"topic/test">>,
emqx_authz_rule:compile(?SOURCE2) emqx_authz_rule:compile({allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]})
) )
), ),
?assertEqual( ?assertEqual(
nomatch, nomatch,
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo2, client_info(#{peerhost => {192, 168, 1, 10}}),
subscribe, #{action_type => subscribe, qos => 0},
<<"#">>, <<"#">>,
emqx_authz_rule:compile(?SOURCE2) emqx_authz_rule:compile({allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]})
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo1, client_info(),
subscribe, #{action_type => subscribe, qos => 0},
<<"test">>, <<"test">>,
emqx_authz_rule:compile(?SOURCE3) emqx_authz_rule:compile(
) {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, [?PH_S_CLIENTID]}
), )
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
ClientInfo2,
subscribe,
<<"test">>,
emqx_authz_rule:compile(?SOURCE3)
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
ClientInfo2,
subscribe,
<<"topic/test">>,
emqx_authz_rule:compile(?SOURCE3)
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo1, client_info(#{peerhost => {192, 168, 1, 10}}),
publish, #{action_type => subscribe, qos => 0},
<<"topic/test">>, <<"test">>,
emqx_authz_rule:compile(?SOURCE4) emqx_authz_rule:compile(
) {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, [?PH_S_CLIENTID]}
), )
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
ClientInfo2,
publish,
<<"topic/test">>,
emqx_authz_rule:compile(?SOURCE4)
) )
), ),
?assertEqual( ?assertEqual(
nomatch, nomatch,
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo3, client_info(#{peerhost => {192, 168, 1, 10}}),
publish, #{action_type => subscribe, qos => 0},
<<"topic/test">>, <<"topic/test">>,
emqx_authz_rule:compile(?SOURCE4) emqx_authz_rule:compile(
) {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, [?PH_S_CLIENTID]}
), )
?assertEqual(
nomatch,
emqx_authz_rule:match(
ClientInfo4,
publish,
<<"topic/test">>,
emqx_authz_rule:compile(?SOURCE4)
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo1, client_info(),
publish, #{action_type => publish, qos => 0, retain => false},
<<"test">>, <<"topic/test">>,
emqx_authz_rule:compile(?SOURCE5) emqx_authz_rule:compile(
{allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}
)
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo2, client_info(#{peerhost => {192, 168, 1, 10}}),
publish, #{action_type => publish, qos => 0, retain => false},
<<"test">>, <<"topic/test">>,
emqx_authz_rule:compile(?SOURCE5) emqx_authz_rule:compile(
{allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}
)
) )
), ),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{username => <<"fake">>}),
#{action_type => publish, qos => 0, retain => false},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}
)
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 0, retain => false},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}
)
)
),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo3, client_info(),
publish, #{action_type => publish, qos => 0, retain => false},
<<"test">>, <<"test">>,
emqx_authz_rule:compile(?SOURCE5) emqx_authz_rule:compile(
{allow,
{'or', [
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
)
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo3, client_info(#{peerhost => {192, 168, 1, 10}}),
publish, #{action_type => publish, qos => 0, retain => false},
<<"test">>,
emqx_authz_rule:compile(
{allow,
{'or', [
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{username => <<"fake">>}),
#{action_type => publish, qos => 0, retain => false},
<<"test">>,
emqx_authz_rule:compile(
{allow,
{'or', [
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{username => <<"fake">>}),
#{action_type => publish, qos => 0, retain => false},
<<"fake">>, <<"fake">>,
emqx_authz_rule:compile(?SOURCE5) emqx_authz_rule:compile(
{allow,
{'or', [
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
)
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo4, client_info(#{clientid => <<"fake">>}),
publish, #{action_type => publish, qos => 0, retain => false},
<<"test">>, <<"test">>,
emqx_authz_rule:compile(?SOURCE5) emqx_authz_rule:compile(
{allow,
{'or', [
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
)
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo4, client_info(#{clientid => <<"fake">>}),
publish, #{action_type => publish, qos => 0, retain => false},
<<"fake">>, <<"fake">>,
emqx_authz_rule:compile(?SOURCE5) emqx_authz_rule:compile(
{allow,
{'or', [
{username, {re, "^test"}},
{clientid, {re, "test?"}}
]},
publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]}
)
) )
), ),
?assertEqual( ?assertEqual(
nomatch, nomatch,
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo1, client_info(),
publish, #{action_type => publish, qos => 0, retain => false},
<<"t/foo${username}boo">>, <<"t/foo${username}boo">>,
emqx_authz_rule:compile(?SOURCE6) emqx_authz_rule:compile({allow, {username, "test"}, publish, ["t/foo${username}boo"]})
) )
), ),
?assertEqual( ?assertEqual(
{matched, allow}, {matched, allow},
emqx_authz_rule:match( emqx_authz_rule:match(
ClientInfo4, client_info(#{clientid => <<"fake">>}),
publish, #{action_type => publish, qos => 0, retain => false},
<<"t/footestboo">>, <<"t/footestboo">>,
emqx_authz_rule:compile(?SOURCE6) emqx_authz_rule:compile({allow, {username, "test"}, publish, ["t/foo${username}boo"]})
) )
), ),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 1, retain => false},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{retain, all}]}, ["topic/test"]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 0, retain => true},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{retain, all}]}, ["topic/test"]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 1, retain => true},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{qos, 1}, {retain, true}]}, ["topic/test"]}
)
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 0, retain => true},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{qos, 1}, {retain, true}]}, ["topic/test"]}
)
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 1, retain => false},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{qos, 1}, {retain, true}]}, ["topic/test"]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => subscribe, qos => 0},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {subscribe, []}, ["topic/test"]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => subscribe, qos => 2},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {subscribe, []}, ["topic/test"]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => subscribe, qos => 1},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {subscribe, [{qos, 1}]}, ["topic/test"]}
)
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => subscribe, qos => 0},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {subscribe, [{qos, 1}]}, ["topic/test"]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => subscribe, qos => 2},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
)
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => subscribe, qos => 0},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
)
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 1, retain => true},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 2, retain => true},
<<"topic/test">>,
emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
)
)
),
?assertEqual(
{matched, allow},
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 2, retain => true},
<<"topic/test">>,
emqx_authz_rule:compile({allow, all, publish, ["#"]})
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => subscribe, qos => 2},
<<"topic/test">>,
emqx_authz_rule:compile({allow, all, publish, ["#"]})
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{username => undefined, peerhost => undefined}),
#{action_type => subscribe, qos => 2},
<<"topic/test">>,
emqx_authz_rule:compile({allow, {username, "user"}, all, ["#"]})
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{username => undefined, peerhost => undefined}),
#{action_type => subscribe, qos => 2},
<<"topic/test">>,
emqx_authz_rule:compile({allow, {ipaddr, "127.0.0.1"}, all, ["#"]})
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{username => undefined, peerhost => undefined}),
#{action_type => subscribe, qos => 2},
<<"topic/test">>,
emqx_authz_rule:compile({allow, {ipaddrs, []}, all, ["#"]})
)
),
?assertEqual(
nomatch,
emqx_authz_rule:match(
client_info(#{clientid => <<"fake">>}),
#{action_type => subscribe, qos => 2},
<<"topic/test">>,
emqx_authz_rule:compile({allow, {clientid, {re, "^test"}}, all, ["#"]})
)
),
ok. ok.
t_invalid_rule(_) ->
?assertThrow(
{invalid_authorization_permission, _},
emqx_authz_rule:compile({allawww, all, all, ["topic/test"]})
),
?assertThrow(
{invalid_authorization_rule, _},
emqx_authz_rule:compile(ooops)
),
?assertThrow(
{invalid_authorization_qos, _},
emqx_authz_rule:compile({allow, {username, "test"}, {publish, [{qos, 3}]}, ["topic/test"]})
),
?assertThrow(
{invalid_authorization_retain, _},
emqx_authz_rule:compile(
{allow, {username, "test"}, {publish, [{retain, 'FALSE'}]}, ["topic/test"]}
)
),
?assertThrow(
{invalid_authorization_action, _},
emqx_authz_rule:compile({allow, all, unsubscribe, ["topic/test"]})
),
?assertThrow(
{invalid_who, _},
emqx_authz_rule:compile({allow, who, all, ["topic/test"]})
).
t_matches(_) ->
?assertEqual(
{matched, allow},
emqx_authz_rule:matches(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 2, retain => true},
<<"topic/test">>,
[
emqx_authz_rule:compile(
{allow, {username, "test"}, {subscribe, [{qos, 1}]}, ["topic/test"]}
),
emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
)
]
)
),
Rule = emqx_authz_rule:compile(
{allow, {username, "test"}, {all, [{qos, 2}, {retain, true}]}, ["topic/test"]}
),
?assertEqual(
nomatch,
emqx_authz_rule:matches(
client_info(#{clientid => <<"fake">>}),
#{action_type => publish, qos => 1, retain => true},
<<"topic/test">>,
[Rule, Rule, Rule]
)
).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
client_info() ->
?CLIENT_INFO_BASE.
client_info(Overrides) ->
maps:merge(?CLIENT_INFO_BASE, Overrides).

View File

@ -0,0 +1,274 @@
%%--------------------------------------------------------------------
%% 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_raw_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
all() ->
emqx_common_test_helpers:all(?MODULE).
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, _Config) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
ok.
t_parse_ok(_Config) ->
lists:foreach(
fun({Expected, RuleRaw}) ->
_ = emqx_authz:set_feature_available(rich_actions, true),
?assertEqual({ok, Expected}, emqx_authz_rule_raw:parse_rule(RuleRaw)),
_ = emqx_authz:set_feature_available(rich_actions, false),
?assertEqual({ok, simple_rule(Expected)}, emqx_authz_rule_raw:parse_rule(RuleRaw))
end,
ok_cases()
).
t_parse_error(_Config) ->
emqx_authz:set_feature_available(rich_actions, true),
lists:foreach(
fun(RuleRaw) ->
?assertMatch(
{error, _},
emqx_authz_rule_raw:parse_rule(RuleRaw)
)
end,
error_cases() ++ error_rich_action_cases()
),
%% without rich actions some fields are not parsed, so they are not errors when invalid
_ = emqx_authz:set_feature_available(rich_actions, false),
lists:foreach(
fun(RuleRaw) ->
?assertMatch(
{error, _},
emqx_authz_rule_raw:parse_rule(RuleRaw)
)
end,
error_cases()
),
lists:foreach(
fun(RuleRaw) ->
?assertMatch(
{ok, _},
emqx_authz_rule_raw:parse_rule(RuleRaw)
)
end,
error_rich_action_cases()
).
t_format(_Config) ->
?assertEqual(
#{
action => subscribe,
permission => allow,
qos => [1, 2],
retain => true,
topic => [<<"a/b/c">>]
},
emqx_authz_rule_raw:format_rule(
{allow, {subscribe, [{qos, [1, 2]}, {retain, true}]}, [<<"a/b/c">>]}
)
),
?assertEqual(
#{
action => publish,
permission => allow,
topic => [<<"a/b/c">>]
},
emqx_authz_rule_raw:format_rule(
{allow, publish, [<<"a/b/c">>]}
)
).
t_format_no_rich_action(_Config) ->
_ = emqx_authz:set_feature_available(rich_actions, false),
Rule = {allow, {subscribe, [{qos, [1, 2]}, {retain, true}]}, [<<"a/b/c">>]},
?assertEqual(
#{action => subscribe, permission => allow, topic => [<<"a/b/c">>]},
emqx_authz_rule_raw:format_rule(Rule)
).
%%--------------------------------------------------------------------
%% Cases
%%--------------------------------------------------------------------
ok_cases() ->
[
{
{allow, {publish, [{qos, [0, 1, 2]}, {retain, all}]}, [<<"a/b/c">>]},
#{
<<"permission">> => <<"allow">>,
<<"topic">> => <<"a/b/c">>,
<<"action">> => <<"publish">>
}
},
{
{deny, {subscribe, [{qos, [1, 2]}]}, [{eq, <<"a/b/c">>}]},
#{
<<"permission">> => <<"deny">>,
<<"topic">> => <<"eq a/b/c">>,
<<"action">> => <<"subscribe">>,
<<"retain">> => <<"true">>,
<<"qos">> => <<"1,2">>
}
},
{
{allow, {publish, [{qos, [0, 1, 2]}, {retain, all}]}, [<<"a">>, <<"b">>]},
#{
<<"permission">> => <<"allow">>,
<<"topics">> => [<<"a">>, <<"b">>],
<<"action">> => <<"publish">>
}
},
{
{allow, {all, [{qos, [0, 1, 2]}, {retain, all}]}, []},
#{
<<"permission">> => <<"allow">>,
<<"topics">> => [],
<<"action">> => <<"all">>
}
},
%% Retain
{
expected_rule_with_qos_retain([0, 1, 2], true),
rule_with_raw_qos_retain(#{<<"retain">> => <<"true">>})
},
{
expected_rule_with_qos_retain([0, 1, 2], true),
rule_with_raw_qos_retain(#{<<"retain">> => true})
},
{
expected_rule_with_qos_retain([0, 1, 2], false),
rule_with_raw_qos_retain(#{<<"retain">> => false})
},
{
expected_rule_with_qos_retain([0, 1, 2], false),
rule_with_raw_qos_retain(#{<<"retain">> => <<"false">>})
},
{
expected_rule_with_qos_retain([0, 1, 2], all),
rule_with_raw_qos_retain(#{<<"retain">> => <<"all">>})
},
{
expected_rule_with_qos_retain([0, 1, 2], all),
rule_with_raw_qos_retain(#{<<"retain">> => undefined})
},
{
expected_rule_with_qos_retain([0, 1, 2], all),
rule_with_raw_qos_retain(#{<<"retain">> => null})
},
{
expected_rule_with_qos_retain([0, 1, 2], all),
rule_with_raw_qos_retain(#{})
},
%% Qos
{
expected_rule_with_qos_retain([2], all),
rule_with_raw_qos_retain(#{<<"qos">> => <<"2">>})
},
{
expected_rule_with_qos_retain([2], all),
rule_with_raw_qos_retain(#{<<"qos">> => [<<"2">>]})
},
{
expected_rule_with_qos_retain([1, 2], all),
rule_with_raw_qos_retain(#{<<"qos">> => <<"1,2">>})
},
{
expected_rule_with_qos_retain([1, 2], all),
rule_with_raw_qos_retain(#{<<"qos">> => [<<"1">>, <<"2">>]})
},
{
expected_rule_with_qos_retain([1, 2], all),
rule_with_raw_qos_retain(#{<<"qos">> => [1, 2]})
},
{
expected_rule_with_qos_retain([0, 1, 2], all),
rule_with_raw_qos_retain(#{<<"qos">> => undefined})
},
{
expected_rule_with_qos_retain([0, 1, 2], all),
rule_with_raw_qos_retain(#{<<"qos">> => null})
}
].
error_cases() ->
[
#{
<<"permission">> => <<"allo">>,
<<"topic">> => <<"a/b/c">>,
<<"action">> => <<"publish">>
},
#{
<<"permission">> => <<"allow">>,
<<"topic">> => <<"a/b/c">>,
<<"action">> => <<"publis">>
},
#{
<<"permission">> => <<"allow">>,
<<"topic">> => #{},
<<"action">> => <<"publish">>
},
#{
<<"permission">> => <<"allow">>,
<<"action">> => <<"publish">>
}
].
error_rich_action_cases() ->
[
#{
<<"permission">> => <<"allow">>,
<<"topics">> => [],
<<"action">> => <<"publish">>,
<<"qos">> => 3
},
#{
<<"permission">> => <<"allow">>,
<<"topics">> => [],
<<"action">> => <<"publish">>,
<<"qos">> => <<"three">>
},
#{
<<"permission">> => <<"allow">>,
<<"topics">> => [],
<<"action">> => <<"publish">>,
<<"retain">> => 3
}
].
expected_rule_with_qos_retain(QoS, Retain) ->
{allow, {publish, [{qos, QoS}, {retain, Retain}]}, []}.
rule_with_raw_qos_retain(Overrides) ->
maps:merge(base_raw_rule(), Overrides).
base_raw_rule() ->
#{
<<"permission">> => <<"allow">>,
<<"topics">> => [],
<<"action">> => <<"publish">>
}.
simple_rule({Pemission, {Action, _Opts}, Topics}) ->
{Pemission, Action, Topics}.

View File

@ -22,8 +22,6 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-compile(export_all). -compile(export_all).
-define(DEFAULT_CHECK_AVAIL_TIMEOUT, 1000).
reset_authorizers() -> reset_authorizers() ->
reset_authorizers(deny, false, []). reset_authorizers(deny, false, []).
@ -53,216 +51,68 @@ setup_config(BaseConfig, SpecialParams) ->
{error, Reason} -> {error, Reason} {error, Reason} -> {error, Reason}
end. end.
test_samples(ClientInfo, Samples) -> %%--------------------------------------------------------------------
%% Table-based test helpers
%%--------------------------------------------------------------------
all_with_table_case(Mod, TableCase, Cases) ->
(emqx_common_test_helpers:all(Mod) -- [TableCase]) ++
[{group, Name} || Name <- case_names(Cases)].
table_groups(TableCase, Cases) ->
[{Name, [], [TableCase]} || Name <- case_names(Cases)].
case_names(Cases) ->
lists:map(fun(Case) -> maps:get(name, Case) end, Cases).
get_case(Name, Cases) ->
[Case] = [C || C <- Cases, maps:get(name, C) =:= Name],
Case.
setup_default_permission(Case) ->
DefaultPermission = maps:get(default_permission, Case, deny),
emqx_authz_test_lib:reset_authorizers(DefaultPermission, false).
base_client_info() ->
#{
clientid => <<"clientid">>,
username => <<"username">>,
peerhost => {127, 0, 0, 1},
zone => default,
listener => {tcp, default}
}.
client_info(Overrides) ->
maps:merge(base_client_info(), Overrides).
enable_features(Case) ->
Features = maps:get(features, Case, []),
lists:foreach( lists:foreach(
fun({Expected, Action, Topic}) -> fun(Feature) ->
ct:pal( Enable = lists:member(Feature, Features),
"client_info: ~p, action: ~p, topic: ~p, expected: ~p", emqx_authz:set_feature_available(Feature, Enable)
[ClientInfo, Action, Topic, Expected]
),
?assertEqual(
Expected,
emqx_access_control:authorize(
ClientInfo,
Action,
Topic
)
)
end, end,
Samples ?AUTHZ_FEATURES
). ).
test_no_topic_rules(ClientInfo, SetupSamples) -> run_checks(#{checks := Checks} = Case) ->
%% No rules _ = setup_default_permission(Case),
_ = enable_features(Case),
ok = reset_authorizers(deny, false), ClientInfoOverrides = maps:get(client_info, Case, #{}),
ok = SetupSamples(ClientInfo, []), ClientInfo = client_info(ClientInfoOverrides),
lists:foreach(
ok = test_samples( fun(Check) ->
ClientInfo, run_check(ClientInfo, Check)
[ end,
{deny, subscribe, <<"#">>}, Checks
{deny, subscribe, <<"subs">>},
{deny, publish, <<"pub">>}
]
). ).
test_allow_topic_rules(ClientInfo, SetupSamples) -> run_check(ClientInfo, {ExpectedPermission, Action, Topic}) ->
Samples = [ ?assertEqual(
#{ ExpectedPermission,
topics => [ emqx_access_control:authorize(
<<"eq testpub1/${username}">>, ClientInfo,
<<"testpub2/${clientid}">>, Action,
<<"testpub3/#">> Topic
], )
permission => <<"allow">>,
action => <<"publish">>
},
#{
topics => [
<<"eq testsub1/${username}">>,
<<"testsub2/${clientid}">>,
<<"testsub3/#">>
],
permission => <<"allow">>,
action => <<"subscribe">>
},
#{
topics => [
<<"eq testall1/${username}">>,
<<"testall2/${clientid}">>,
<<"testall3/#">>
],
permission => <<"allow">>,
action => <<"all">>
}
],
ok = reset_authorizers(deny, false),
ok = SetupSamples(ClientInfo, Samples),
ok = test_samples(
ClientInfo,
[
%% Publish rules
{deny, publish, <<"testpub1/username">>},
{allow, publish, <<"testpub1/${username}">>},
{allow, publish, <<"testpub2/clientid">>},
{allow, publish, <<"testpub3/foobar">>},
{deny, publish, <<"testpub2/username">>},
{deny, publish, <<"testpub1/clientid">>},
{deny, subscribe, <<"testpub1/username">>},
{deny, subscribe, <<"testpub2/clientid">>},
{deny, subscribe, <<"testpub3/foobar">>},
%% Subscribe rules
{deny, subscribe, <<"testsub1/username">>},
{allow, subscribe, <<"testsub1/${username}">>},
{allow, subscribe, <<"testsub2/clientid">>},
{allow, subscribe, <<"testsub3/foobar">>},
{allow, subscribe, <<"testsub3/+/foobar">>},
{allow, subscribe, <<"testsub3/#">>},
{deny, subscribe, <<"testsub2/username">>},
{deny, subscribe, <<"testsub1/clientid">>},
{deny, subscribe, <<"testsub4/foobar">>},
{deny, publish, <<"testsub1/username">>},
{deny, publish, <<"testsub2/clientid">>},
{deny, publish, <<"testsub3/foobar">>},
%% All rules
{deny, subscribe, <<"testall1/username">>},
{allow, subscribe, <<"testall1/${username}">>},
{allow, subscribe, <<"testall2/clientid">>},
{allow, subscribe, <<"testall3/foobar">>},
{allow, subscribe, <<"testall3/+/foobar">>},
{allow, subscribe, <<"testall3/#">>},
{deny, publish, <<"testall1/username">>},
{allow, publish, <<"testall1/${username}">>},
{allow, publish, <<"testall2/clientid">>},
{allow, publish, <<"testall3/foobar">>},
{deny, subscribe, <<"testall2/username">>},
{deny, subscribe, <<"testall1/clientid">>},
{deny, subscribe, <<"testall4/foobar">>},
{deny, publish, <<"testall2/username">>},
{deny, publish, <<"testall1/clientid">>},
{deny, publish, <<"testall4/foobar">>}
]
).
test_deny_topic_rules(ClientInfo, SetupSamples) ->
Samples = [
#{
topics => [
<<"eq testpub1/${username}">>,
<<"testpub2/${clientid}">>,
<<"testpub3/#">>
],
permission => <<"deny">>,
action => <<"publish">>
},
#{
topics => [
<<"eq testsub1/${username}">>,
<<"testsub2/${clientid}">>,
<<"testsub3/#">>
],
permission => <<"deny">>,
action => <<"subscribe">>
},
#{
topics => [
<<"eq testall1/${username}">>,
<<"testall2/${clientid}">>,
<<"testall3/#">>
],
permission => <<"deny">>,
action => <<"all">>
}
],
ok = reset_authorizers(allow, false),
ok = SetupSamples(ClientInfo, Samples),
ok = test_samples(
ClientInfo,
[
%% Publish rules
{allow, publish, <<"testpub1/username">>},
{deny, publish, <<"testpub1/${username}">>},
{deny, publish, <<"testpub2/clientid">>},
{deny, publish, <<"testpub3/foobar">>},
{allow, publish, <<"testpub2/username">>},
{allow, publish, <<"testpub1/clientid">>},
{allow, subscribe, <<"testpub1/username">>},
{allow, subscribe, <<"testpub2/clientid">>},
{allow, subscribe, <<"testpub3/foobar">>},
%% Subscribe rules
{allow, subscribe, <<"testsub1/username">>},
{deny, subscribe, <<"testsub1/${username}">>},
{deny, subscribe, <<"testsub2/clientid">>},
{deny, subscribe, <<"testsub3/foobar">>},
{deny, subscribe, <<"testsub3/+/foobar">>},
{deny, subscribe, <<"testsub3/#">>},
{allow, subscribe, <<"testsub2/username">>},
{allow, subscribe, <<"testsub1/clientid">>},
{allow, subscribe, <<"testsub4/foobar">>},
{allow, publish, <<"testsub1/username">>},
{allow, publish, <<"testsub2/clientid">>},
{allow, publish, <<"testsub3/foobar">>},
%% All rules
{allow, subscribe, <<"testall1/username">>},
{deny, subscribe, <<"testall1/${username}">>},
{deny, subscribe, <<"testall2/clientid">>},
{deny, subscribe, <<"testall3/foobar">>},
{deny, subscribe, <<"testall3/+/foobar">>},
{deny, subscribe, <<"testall3/#">>},
{allow, publish, <<"testall1/username">>},
{deny, publish, <<"testall1/${username}">>},
{deny, publish, <<"testall2/clientid">>},
{deny, publish, <<"testall3/foobar">>},
{allow, subscribe, <<"testall2/username">>},
{allow, subscribe, <<"testall1/clientid">>},
{allow, subscribe, <<"testall4/foobar">>},
{allow, publish, <<"testall2/username">>},
{allow, publish, <<"testall1/clientid">>},
{allow, publish, <<"testall4/foobar">>}
]
). ).

View File

@ -1,7 +1,7 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
{application, emqx_exhook, [ {application, emqx_exhook, [
{description, "EMQX Extension for Hook"}, {description, "EMQX Extension for Hook"},
{vsn, "5.0.13"}, {vsn, "5.0.14"},
{modules, []}, {modules, []},
{registered, []}, {registered, []},
{mod, {emqx_exhook_app, []}}, {mod, {emqx_exhook_app, []}},

View File

@ -16,9 +16,9 @@
-module(emqx_exhook_handler). -module(emqx_exhook_handler).
-include("emqx_exhook.hrl").
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-export([ -export([
on_client_connect/2, on_client_connect/2,
@ -132,12 +132,13 @@ on_client_authenticate(ClientInfo, AuthResult) ->
{ok, AuthResult} {ok, AuthResult}
end. end.
on_client_authorize(ClientInfo, PubSub, Topic, Result) -> on_client_authorize(ClientInfo, Action, Topic, Result) ->
Bool = maps:get(result, Result, deny) == allow, Bool = maps:get(result, Result, deny) == allow,
%% TODO: Support full action in major release
Type = Type =
case PubSub of case Action of
publish -> 'PUBLISH'; ?authz_action(publish) -> 'PUBLISH';
subscribe -> 'SUBSCRIBE' ?authz_action(subscribe) -> 'SUBSCRIBE'
end, end,
Req = #{ Req = #{
clientinfo => clientinfo(ClientInfo), clientinfo => clientinfo(ClientInfo),

View File

@ -19,7 +19,7 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include("emqx_exhook.hrl"). -include_lib("emqx/include/emqx_access_control.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
@ -126,7 +126,7 @@ t_access_failed_if_no_server_running(Config) ->
allow, allow,
emqx_access_control:authorize( emqx_access_control:authorize(
ClientInfo#{username => <<"gooduser">>}, ClientInfo#{username => <<"gooduser">>},
publish, ?AUTHZ_PUBLISH,
<<"acl/1">> <<"acl/1">>
) )
), ),
@ -135,7 +135,7 @@ t_access_failed_if_no_server_running(Config) ->
deny, deny,
emqx_access_control:authorize( emqx_access_control:authorize(
ClientInfo#{username => <<"baduser">>}, ClientInfo#{username => <<"baduser">>},
publish, ?AUTHZ_PUBLISH,
<<"acl/2">> <<"acl/2">>
) )
), ),
@ -148,7 +148,7 @@ t_access_failed_if_no_server_running(Config) ->
?assertMatch( ?assertMatch(
{stop, #{result := deny, from := exhook}}, {stop, #{result := deny, from := exhook}},
emqx_exhook_handler:on_client_authorize(ClientInfo, publish, <<"t/1">>, #{ emqx_exhook_handler:on_client_authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t/1">>, #{
result => allow, from => exhook result => allow, from => exhook
}) })
), ),

View File

@ -18,6 +18,7 @@
-include_lib("proper/include/proper.hrl"). -include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-import( -import(
emqx_proper_types, emqx_proper_types,
@ -29,7 +30,8 @@
connack_return_code/0, connack_return_code/0,
topictab/0, topictab/0,
topic/0, topic/0,
subopts/0 subopts/0,
pubsub/0
] ]
). ).
@ -138,7 +140,7 @@ prop_client_authorize() ->
{ClientInfo0, PubSub, Topic, Result, Meta}, {ClientInfo0, PubSub, Topic, Result, Meta},
{ {
clientinfo(), clientinfo(),
oneof([publish, subscribe]), pubsub(),
topic(), topic(),
oneof([MkResult(allow), MkResult(deny)]), oneof([MkResult(allow), MkResult(deny)]),
request_meta() request_meta()
@ -554,8 +556,8 @@ authresult_to_bool(AuthResult) ->
aclresult_to_bool(#{result := Result}) -> aclresult_to_bool(#{result := Result}) ->
Result == allow. Result == allow.
pubsub_to_enum(publish) -> 'PUBLISH'; pubsub_to_enum(?authz_action(publish)) -> 'PUBLISH';
pubsub_to_enum(subscribe) -> 'SUBSCRIBE'. pubsub_to_enum(?authz_action(subscribe)) -> 'SUBSCRIBE'.
from_conninfo(ConnInfo) -> from_conninfo(ConnInfo) ->
#{ #{

View File

@ -162,8 +162,8 @@ connection_closed(_Ctx = #{gwname := GwName}, ClientId) ->
emqx_types:topic() emqx_types:topic()
) -> ) ->
allow | deny. allow | deny.
authorize(_Ctx, ClientInfo, PubSub, Topic) -> authorize(_Ctx, ClientInfo, Action, Topic) ->
emqx_access_control:authorize(ClientInfo, PubSub, Topic). emqx_access_control:authorize(ClientInfo, Action, Topic).
metrics_inc(_Ctx = #{gwname := GwName}, Name) -> metrics_inc(_Ctx = #{gwname := GwName}, Name) ->
emqx_gateway_metrics:inc(GwName, Name). emqx_gateway_metrics:inc(GwName, Name).

View File

@ -47,9 +47,6 @@
-include("emqx_coap.hrl"). -include("emqx_coap.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_authentication.hrl").
-define(AUTHN, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
-record(channel, { -record(channel, {
%% Context %% Context
@ -166,8 +163,8 @@ init(
conn_state = idle conn_state = idle
}. }.
validator(Type, Topic, Ctx, ClientInfo) -> validator(Action, Topic, Ctx, ClientInfo) ->
emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, Topic).
-spec send_request(pid(), coap_message()) -> any(). -spec send_request(pid(), coap_message()) -> any().
send_request(Channel, Request) -> send_request(Channel, Request) ->

View File

@ -18,6 +18,7 @@
-module(emqx_coap_pubsub_handler). -module(emqx_coap_pubsub_handler).
-include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-include("emqx_coap.hrl"). -include("emqx_coap.hrl").
-export([handle_request/4]). -export([handle_request/4]).
@ -50,14 +51,16 @@ handle_method(get, Topic, Msg, Ctx, CInfo) ->
reply({error, bad_request}, <<"invalid observe value">>, Msg) reply({error, bad_request}, <<"invalid observe value">>, Msg)
end; end;
handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) -> handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) ->
case emqx_coap_channel:validator(publish, Topic, Ctx, CInfo) of PublishOpts = get_publish_opts(Msg),
Qos = get_publish_qos(Msg, PublishOpts),
Action = ?AUTHZ_PUBLISH(Qos, get_publish_retain(PublishOpts)),
case emqx_coap_channel:validator(Action, Topic, Ctx, CInfo) of
allow -> allow ->
#{clientid := ClientId} = CInfo, #{clientid := ClientId} = CInfo,
MountTopic = mount(CInfo, Topic), MountTopic = mount(CInfo, Topic),
QOS = get_publish_qos(Msg),
%% TODO: Append message metadata into headers %% TODO: Append message metadata into headers
MQTTMsg = emqx_message:make(ClientId, QOS, MountTopic, Payload), MQTTMsg = emqx_message:make(ClientId, Qos, MountTopic, Payload),
MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg), MQTTMsg2 = apply_publish_opts(PublishOpts, MQTTMsg),
_ = emqx_broker:publish(MQTTMsg2), _ = emqx_broker:publish(MQTTMsg2),
reply({ok, changed}, Msg); reply({ok, changed}, Msg);
_ -> _ ->
@ -104,48 +107,70 @@ type_to_qos(coap, #coap_message{type = Type}) ->
?QOS_1 ?QOS_1
end. end.
get_publish_qos(Msg) -> get_publish_opts(Msg) ->
case emqx_coap_message:get_option(uri_query, Msg) of
#{<<"qos">> := QOS} ->
erlang:binary_to_integer(QOS);
_ ->
CfgType = emqx_conf:get([gateway, coap, publish_qos], ?QOS_0),
type_to_qos(CfgType, Msg)
end.
apply_publish_opts(Msg, MQTTMsg) ->
case emqx_coap_message:get_option(uri_query, Msg) of case emqx_coap_message:get_option(uri_query, Msg) of
undefined -> undefined ->
MQTTMsg; #{};
Qs -> Qs ->
maps:fold( maps:fold(
fun fun
(<<"retain">>, V, Acc) -> (<<"retain">>, V, Acc) ->
Val = V =:= <<"true">>, Val = V =:= <<"true">>,
emqx_message:set_flag(retain, Val, Acc); Acc#{retain => Val};
(<<"expiry">>, V, Acc) -> (<<"expiry">>, V, Acc) ->
Val = erlang:binary_to_integer(V), Val = erlang:binary_to_integer(V),
Props = emqx_message:get_header(properties, Acc), Acc#{expiry_interval => Val};
emqx_message:set_header( (<<"qos">>, V, Acc) ->
properties, Val = erlang:binary_to_integer(V),
Props#{'Message-Expiry-Interval' => Val}, Acc#{qos => Val};
Acc
);
(_, _, Acc) -> (_, _, Acc) ->
Acc Acc
end, end,
MQTTMsg, #{},
Qs Qs
) )
end. end.
get_publish_qos(Msg, PublishOpts) ->
case PublishOpts of
#{qos := Qos} ->
Qos;
_ ->
CfgType = emqx_conf:get([gateway, coap, publish_qos], ?QOS_0),
type_to_qos(CfgType, Msg)
end.
get_publish_retain(PublishOpts) ->
maps:get(retain, PublishOpts, false).
apply_publish_opts(Opts, MQTTMsg) ->
maps:fold(
fun
(retain, Val, Acc) ->
emqx_message:set_flag(retain, Val, Acc);
(expiry, Val, Acc) ->
Props = emqx_message:get_header(properties, Acc),
emqx_message:set_header(
properties,
Props#{'Message-Expiry-Interval' => Val},
Acc
);
(_, _, Acc) ->
Acc
end,
MQTTMsg,
Opts
).
subscribe(#coap_message{token = <<>>} = Msg, _, _, _) -> subscribe(#coap_message{token = <<>>} = Msg, _, _, _) ->
reply({error, bad_request}, <<"observe without token">>, Msg); reply({error, bad_request}, <<"observe without token">>, Msg);
subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) -> subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) ->
case emqx_coap_channel:validator(subscribe, Topic, Ctx, CInfo) of #{qos := Qos} = SubOpts = get_sub_opts(Msg),
Action = ?AUTHZ_SUBSCRIBE(Qos),
case emqx_coap_channel:validator(Action, Topic, Ctx, CInfo) of
allow -> allow ->
#{clientid := ClientId} = CInfo, #{clientid := ClientId} = CInfo,
SubOpts = get_sub_opts(Msg),
MountTopic = mount(CInfo, Topic), MountTopic = mount(CInfo, Topic),
emqx_broker:subscribe(MountTopic, ClientId, SubOpts), emqx_broker:subscribe(MountTopic, ClientId, SubOpts),
run_hooks(Ctx, 'session.subscribed', [CInfo, MountTopic, SubOpts]), run_hooks(Ctx, 'session.subscribed', [CInfo, MountTopic, SubOpts]),

View File

@ -19,6 +19,7 @@
-include("emqx_exproto.hrl"). -include("emqx_exproto.hrl").
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-include_lib("emqx/include/types.hrl"). -include_lib("emqx/include/types.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
@ -428,7 +429,8 @@ handle_call(
clientinfo = ClientInfo clientinfo = ClientInfo
} }
) -> ) ->
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, TopicFilter) of Action = ?AUTHZ_SUBSCRIBE(Qos),
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, TopicFilter) of
deny -> deny ->
{reply, {error, ?RESP_PERMISSION_DENY, <<"Authorization deny">>}, Channel}; {reply, {error, ?RESP_PERMISSION_DENY, <<"Authorization deny">>}, Channel};
_ -> _ ->
@ -464,7 +466,8 @@ handle_call(
} }
} }
) -> ) ->
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of Action = ?AUTHZ_PUBLISH(Qos),
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, Topic) of
deny -> deny ->
{reply, {error, ?RESP_PERMISSION_DENY, <<"Authorization deny">>}, Channel}; {reply, {error, ?RESP_PERMISSION_DENY, <<"Authorization deny">>}, Channel};
_ -> _ ->

View File

@ -17,7 +17,9 @@
-module(emqx_lwm2m_channel). -module(emqx_lwm2m_channel).
-include("emqx_lwm2m.hrl"). -include("emqx_lwm2m.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-include_lib("emqx_gateway_coap/include/emqx_coap.hrl"). -include_lib("emqx_gateway_coap/include/emqx_coap.hrl").
%% API %% API
@ -644,7 +646,8 @@ with_context(Ctx, ClientInfo) ->
end. end.
with_context(publish, [Topic, Msg], Ctx, ClientInfo) -> with_context(publish, [Topic, Msg], Ctx, ClientInfo) ->
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of Action = publish_action(Msg),
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, Topic) of
allow -> allow ->
_ = emqx_broker:publish(Msg), _ = emqx_broker:publish(Msg),
ok; ok;
@ -660,7 +663,8 @@ with_context(subscribe, [Topic, Opts], Ctx, ClientInfo) ->
clientid := ClientId, clientid := ClientId,
endpoint_name := EndpointName endpoint_name := EndpointName
} = ClientInfo, } = ClientInfo,
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic) of Action = subscribe_action(Opts),
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, Topic) of
allow -> allow ->
run_hooks(Ctx, 'session.subscribed', [ClientInfo, Topic, Opts]), run_hooks(Ctx, 'session.subscribed', [ClientInfo, Topic, Opts]),
?SLOG(debug, #{ ?SLOG(debug, #{
@ -681,6 +685,14 @@ with_context(subscribe, [Topic, Opts], Ctx, ClientInfo) ->
with_context(metrics, Name, Ctx, _ClientInfo) -> with_context(metrics, Name, Ctx, _ClientInfo) ->
emqx_gateway_ctx:metrics_inc(Ctx, Name). emqx_gateway_ctx:metrics_inc(Ctx, Name).
publish_action(#message{qos = QoS, flags = Flags}) ->
Retain = maps:get(retain, Flags, false),
?AUTHZ_PUBLISH(QoS, Retain).
subscribe_action(Opts) ->
QoS = maps:get(qos, Opts, 0),
?AUTHZ_SUBSCRIBE(QoS).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Call Chain %% Call Chain
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -1,6 +1,6 @@
{application, emqx_gateway_mqttsn, [ {application, emqx_gateway_mqttsn, [
{description, "MQTT-SN Gateway"}, {description, "MQTT-SN Gateway"},
{vsn, "0.1.2"}, {vsn, "0.1.3"},
{registered, []}, {registered, []},
{applications, [kernel, stdlib, emqx, emqx_gateway]}, {applications, [kernel, stdlib, emqx, emqx_gateway]},
{env, []}, {env, []},

View File

@ -22,6 +22,7 @@
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/types.hrl"). -include_lib("emqx/include/types.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
@ -1099,10 +1100,11 @@ convert_topic_id_to_name(
end. end.
check_pub_authz( check_pub_authz(
{TopicName, _Flags, _Data}, {TopicName, #mqtt_sn_flags{qos = QoS, retain = Retain}, _Data},
#channel{ctx = Ctx, clientinfo = ClientInfo} #channel{ctx = Ctx, clientinfo = ClientInfo}
) -> ) ->
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, TopicName) of Action = ?AUTHZ_PUBLISH(QoS, Retain),
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, TopicName) of
allow -> ok; allow -> ok;
deny -> {error, ?SN_RC2_NOT_AUTHORIZE} deny -> {error, ?SN_RC2_NOT_AUTHORIZE}
end. end.
@ -1251,10 +1253,11 @@ preproc_subs_type(
{error, ?SN_RC_NOT_SUPPORTED}. {error, ?SN_RC_NOT_SUPPORTED}.
check_subscribe_authz( check_subscribe_authz(
{_TopicId, TopicName, _QoS}, {_TopicId, TopicName, QoS},
Channel = #channel{ctx = Ctx, clientinfo = ClientInfo} Channel = #channel{ctx = Ctx, clientinfo = ClientInfo}
) -> ) ->
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, TopicName) of Action = ?AUTHZ_SUBSCRIBE(QoS),
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, TopicName) of
allow -> allow ->
{ok, Channel}; {ok, Channel};
_ -> _ ->

View File

@ -20,6 +20,7 @@
-include("emqx_stomp.hrl"). -include("emqx_stomp.hrl").
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-import(proplists, [get_value/2, get_value/3]). -import(proplists, [get_value/2, get_value/3]).
@ -446,7 +447,10 @@ handle_in(
} }
) -> ) ->
Topic = header(<<"destination">>, Headers), Topic = header(<<"destination">>, Headers),
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of %% Flags and QoS are not supported in STOMP anyway,
%% no need to look into the frame
Action = ?AUTHZ_PUBLISH,
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, Topic) of
deny -> deny ->
ErrMsg = io_lib:format("Insufficient permissions for ~s", [Topic]), ErrMsg = io_lib:format("Insufficient permissions for ~s", [Topic]),
ErrorFrame = error_frame(receipt_id(Headers), ErrMsg), ErrorFrame = error_frame(receipt_id(Headers), ErrMsg),
@ -717,7 +721,9 @@ check_sub_acl(
clientinfo = ClientInfo clientinfo = ClientInfo
} }
) -> ) ->
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, ParsedTopic) of %% QoS is not supported in stomp
Action = ?AUTHZ_SUBSCRIBE,
case emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, ParsedTopic) of
deny -> {error, acl_denied}; deny -> {error, acl_denied};
allow -> ok allow -> ok
end. end.

View File

@ -89,15 +89,15 @@ t_clients(_) ->
AfterKickoutResponse2 = emqx_mgmt_api_test_util:request_api(get, Client2Path), AfterKickoutResponse2 = emqx_mgmt_api_test_util:request_api(get, Client2Path),
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2), ?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2),
%% get /clients/:clientid/authorization/cache should has no authz cache %% get /clients/:clientid/authorization/cache should have no authz cache
Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path([ Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path([
"clients", "clients",
binary_to_list(ClientId1), binary_to_list(ClientId1),
"authorization", "authorization",
"cache" "cache"
]), ]),
{ok, Client1AuthzCache} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath), {ok, Client1AuthzCache0} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath),
?assertEqual("[]", Client1AuthzCache), ?assertEqual("[]", Client1AuthzCache0),
%% post /clients/:clientid/subscribe %% post /clients/:clientid/subscribe
SubscribeBody = #{topic => Topic, qos => Qos, nl => 1, rh => 1}, SubscribeBody = #{topic => Topic, qos => Qos, nl => 1, rh => 1},
@ -167,6 +167,35 @@ t_clients(_) ->
AfterKickoutResponse1 = emqx_mgmt_api_test_util:request_api(get, Client1Path), AfterKickoutResponse1 = emqx_mgmt_api_test_util:request_api(get, Client1Path),
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse1). ?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse1).
t_authz_cache(_) ->
ClientId = <<"client_authz">>,
{ok, C} = emqtt:start_link(#{clientid => ClientId}),
{ok, _} = emqtt:connect(C),
{ok, _, _} = emqtt:subscribe(C, <<"topic/1">>, 0),
ClientAuthzCachePath = emqx_mgmt_api_test_util:api_path([
"clients",
binary_to_list(ClientId),
"authorization",
"cache"
]),
{ok, ClientAuthzCache} = emqx_mgmt_api_test_util:request_api(get, ClientAuthzCachePath),
?assertMatch(
[
#{
<<"access">> :=
#{<<"action_type">> := <<"subscribe">>, <<"qos">> := 1},
<<"result">> := <<"allow">>,
<<"topic">> := <<"topic/1">>,
<<"updated_time">> := _
}
],
emqx_utils_json:decode(ClientAuthzCache, [return_maps])
),
ok = emqtt:stop(C).
t_kickout_clients(_) -> t_kickout_clients(_) ->
process_flag(trap_exit, true), process_flag(trap_exit, true),

View File

@ -20,6 +20,7 @@
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/emqx_hooks.hrl"). -include_lib("emqx/include/emqx_hooks.hrl").
-include_lib("emqx/include/emqx_access_control.hrl").
-include_lib("emqx_bridge/include/emqx_bridge_resource.hrl"). -include_lib("emqx_bridge/include/emqx_bridge_resource.hrl").
-export([ -export([
@ -160,7 +161,10 @@ on_client_connack(ConnInfo, Reason, _, Conf) ->
Conf Conf
). ).
on_client_check_authz_complete(ClientInfo, PubSub, Topic, Result, AuthzSource, Conf) -> %% TODO: support full action in major release
on_client_check_authz_complete(
ClientInfo, ?authz_action(PubSub), Topic, Result, AuthzSource, Conf
) ->
apply_event( apply_event(
'client.check_authz_complete', 'client.check_authz_complete',
fun() -> fun() ->

View File

@ -0,0 +1,2 @@
Add support for MQTT action authorization based on QoS level and Retain flag values.
Now, EMQX can check by ACL whether a client has permission to publish/subscribe using a specified QoS level and to use retained messages.

View File

@ -1,10 +1,20 @@
emqx_authz_api_mnesia { emqx_authz_api_mnesia {
action.desc: action.desc:
"""Authorized action (pub/sub/all)""" """Authorized action (publish/subscribe/all)"""
action.label: action.label:
"""action""" """action"""
qos.desc:
"""QoS of authorized action"""
qos.label:
"""QoS"""
retain.desc:
"""Retain flag of authorized action"""
retain.label:
"""retain"""
clientid.desc: clientid.desc:
"""ClientID""" """ClientID"""
clientid.label: clientid.label: