From 7de26a17765fffdac1e6d3d37507b58f1974c48d Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Thu, 1 Jun 2023 23:19:19 +0300 Subject: [PATCH] feat(authz): use extensible map format for actions in authz rules * support authorization on retain, qos fields * refactored authz tests heavily --- apps/emqx/include/emqx_access_control.hrl | 14 + apps/emqx/include/emqx_placeholder.hrl | 4 +- apps/emqx/src/emqx_access_control.erl | 28 +- apps/emqx/src/emqx_authz_cache.erl | 5 +- apps/emqx/src/emqx_channel.erl | 47 +- apps/emqx/src/emqx_types.erl | 8 +- apps/emqx/test/emqx_access_control_SUITE.erl | 17 +- apps/emqx/test/emqx_authz_cache_SUITE.erl | 2 - apps/emqx/test/emqx_channel_SUITE.erl | 3 +- apps/emqx/test/emqx_proper_types.erl | 21 +- apps/emqx_authz/include/emqx_authz.hrl | 62 +- apps/emqx_authz/src/emqx_authz.erl | 27 + apps/emqx_authz/src/emqx_authz_api_mnesia.erl | 101 +-- apps/emqx_authz/src/emqx_authz_file.erl | 1 - apps/emqx_authz/src/emqx_authz_http.erl | 35 +- apps/emqx_authz/src/emqx_authz_mnesia.erl | 28 +- apps/emqx_authz/src/emqx_authz_mongodb.erl | 26 +- apps/emqx_authz/src/emqx_authz_mysql.erl | 52 +- apps/emqx_authz/src/emqx_authz_postgresql.erl | 51 +- apps/emqx_authz/src/emqx_authz_redis.erl | 58 +- apps/emqx_authz/src/emqx_authz_rule.erl | 183 +++-- apps/emqx_authz/src/emqx_authz_rule_raw.erl | 197 +++++ apps/emqx_authz/src/emqx_authz_utils.erl | 30 +- .../test/emqx_authz_api_mnesia_SUITE.erl | 6 +- .../emqx_authz/test/emqx_authz_file_SUITE.erl | 65 +- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 93 ++- apps/emqx_authz/test/emqx_authz_jwt_SUITE.erl | 5 +- .../test/emqx_authz_mnesia_SUITE.erl | 189 +++-- .../test/emqx_authz_mongodb_SUITE.erl | 516 +++++++------ .../test/emqx_authz_mysql_SUITE.erl | 564 +++++++------- .../test/emqx_authz_postgresql_SUITE.erl | 582 ++++++++------- .../test/emqx_authz_redis_SUITE.erl | 363 +++++---- .../emqx_authz/test/emqx_authz_rule_SUITE.erl | 696 ++++++++++++++---- .../test/emqx_authz_rule_raw_SUITE.erl | 274 +++++++ apps/emqx_authz/test/emqx_authz_test_lib.erl | 266 ++----- apps/emqx_exhook/src/emqx_exhook.app.src | 2 +- apps/emqx_exhook/src/emqx_exhook_handler.erl | 11 +- apps/emqx_exhook/test/emqx_exhook_SUITE.erl | 8 +- .../test/props/prop_exhook_hooks.erl | 10 +- apps/emqx_gateway/src/emqx_gateway_ctx.erl | 4 +- .../src/emqx_coap_channel.erl | 7 +- .../src/emqx_coap_pubsub_handler.erl | 75 +- .../src/emqx_exproto_channel.erl | 7 +- .../src/emqx_lwm2m_channel.erl | 16 +- .../src/emqx_gateway_mqttsn.app.src | 2 +- .../src/emqx_mqttsn_channel.erl | 11 +- .../src/emqx_stomp_channel.erl | 10 +- .../test/emqx_mgmt_api_clients_SUITE.erl | 35 +- .../emqx_rule_engine/src/emqx_rule_events.erl | 6 +- changes/ee/feat-11132.en.md | 2 + rel/i18n/emqx_authz_api_mnesia.hocon | 12 +- 51 files changed, 3144 insertions(+), 1693 deletions(-) create mode 100644 apps/emqx_authz/src/emqx_authz_rule_raw.erl create mode 100644 apps/emqx_authz/test/emqx_authz_rule_raw_SUITE.erl create mode 100644 changes/ee/feat-11132.en.md diff --git a/apps/emqx/include/emqx_access_control.hrl b/apps/emqx/include/emqx_access_control.hrl index 693bc91b5..e840d2b4a 100644 --- a/apps/emqx/include/emqx_access_control.hrl +++ b/apps/emqx/include/emqx_access_control.hrl @@ -18,3 +18,17 @@ -define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME, "authorization"). -define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_ATOM, 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(_)). diff --git a/apps/emqx/include/emqx_placeholder.hrl b/apps/emqx/include/emqx_placeholder.hrl index d5da3fb18..7b2ce6c6b 100644 --- a/apps/emqx/include/emqx_placeholder.hrl +++ b/apps/emqx/include/emqx_placeholder.hrl @@ -21,7 +21,7 @@ -define(PH(Type), <<"${", Type/binary, "}">>). -%% action: publish/subscribe/all +%% action: publish/subscribe -define(PH_ACTION, <<"${action}">>). %% cert @@ -79,6 +79,7 @@ -define(PH_REASON, <<"${reason}">>). -define(PH_ENDPOINT_NAME, <<"${endpoint_name}">>). +-define(PH_RETAIN, <<"${retain}">>). %% sync change these place holder with binary def. -define(PH_S_ACTION, "${action}"). @@ -113,5 +114,6 @@ -define(PH_S_NODE, "${node}"). -define(PH_S_REASON, "${reason}"). -define(PH_S_ENDPOINT_NAME, "${endpoint_name}"). +-define(PH_S_RETAIN, "${retain}"). -endif. diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index efe9bee37..43669bf6c 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -77,10 +77,10 @@ authenticate(Credential) -> %% @doc Check Authorization -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny. -authorize(ClientInfo, PubSub, <<"$delayed/", Data/binary>> = RawTopic) -> +authorize(ClientInfo, Action, <<"$delayed/", Data/binary>> = RawTopic) -> case binary:split(Data, <<"/">>) of [_, Topic] -> - authorize(ClientInfo, PubSub, Topic); + authorize(ClientInfo, Action, Topic); _ -> ?SLOG(warning, #{ msg => "invalid_delayed_topic_format", @@ -90,39 +90,39 @@ authorize(ClientInfo, PubSub, <<"$delayed/", Data/binary>> = RawTopic) -> inc_authz_metrics(deny), deny end; -authorize(ClientInfo, PubSub, Topic) -> +authorize(ClientInfo, Action, Topic) -> Result = case emqx_authz_cache:is_enabled() of - true -> check_authorization_cache(ClientInfo, PubSub, Topic); - false -> do_authorize(ClientInfo, PubSub, Topic) + true -> check_authorization_cache(ClientInfo, Action, Topic); + false -> do_authorize(ClientInfo, Action, Topic) end, inc_authz_metrics(Result), Result. -check_authorization_cache(ClientInfo, PubSub, Topic) -> - case emqx_authz_cache:get_authz_cache(PubSub, Topic) of +check_authorization_cache(ClientInfo, Action, Topic) -> + case emqx_authz_cache:get_authz_cache(Action, Topic) of not_found -> - AuthzResult = do_authorize(ClientInfo, PubSub, Topic), - emqx_authz_cache:put_authz_cache(PubSub, Topic, AuthzResult), + AuthzResult = do_authorize(ClientInfo, Action, Topic), + emqx_authz_cache:put_authz_cache(Action, Topic, AuthzResult), AuthzResult; AuthzResult -> emqx:run_hook( 'client.check_authz_complete', - [ClientInfo, PubSub, Topic, AuthzResult, cache] + [ClientInfo, Action, Topic, AuthzResult, cache] ), inc_authz_metrics(cache_hit), AuthzResult end. -do_authorize(ClientInfo, PubSub, Topic) -> +do_authorize(ClientInfo, Action, Topic) -> NoMatch = emqx:get_config([authorization, no_match], allow), 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 -> From = maps:get(from, AuthzResult, unknown), emqx:run_hook( 'client.check_authz_complete', - [ClientInfo, PubSub, Topic, Result, From] + [ClientInfo, Action, Topic, Result, From] ), Result; Other -> @@ -133,7 +133,7 @@ do_authorize(ClientInfo, PubSub, Topic) -> }), emqx:run_hook( 'client.check_authz_complete', - [ClientInfo, PubSub, Topic, deny, unknown_return_format] + [ClientInfo, Action, Topic, deny, unknown_return_format] ), deny end. diff --git a/apps/emqx/src/emqx_authz_cache.erl b/apps/emqx/src/emqx_authz_cache.erl index 6555266a5..af19ecf8f 100644 --- a/apps/emqx/src/emqx_authz_cache.erl +++ b/apps/emqx/src/emqx_authz_cache.erl @@ -16,7 +16,7 @@ -module(emqx_authz_cache). --include("emqx.hrl"). +-include("emqx_access_control.hrl"). -export([ list_authz_cache/0, @@ -159,8 +159,7 @@ dump_authz_cache() -> map_authz_cache(Fun) -> [ Fun(R) - || R = {{SubPub, _T}, _Authz} <- erlang:get(), - SubPub =:= publish orelse SubPub =:= subscribe + || R = {{?authz_action, _T}, _Authz} <- erlang:get() ]. foreach_authz_cache(Fun) -> _ = map_authz_cache(Fun), diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 93bb6535e..01af1a7b5 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -20,6 +20,7 @@ -include("emqx.hrl"). -include("emqx_channel.hrl"). -include("emqx_mqtt.hrl"). +-include("emqx_access_control.hrl"). -include("logger.hrl"). -include("types.hrl"). @@ -491,7 +492,7 @@ handle_in( ok -> TopicFilters0 = parse_topic_filters(TopicFilters), TopicFilters1 = enrich_subopts_subid(Properties, TopicFilters0), - TupleTopicFilters0 = check_sub_authzs(TopicFilters1, Channel), + TupleTopicFilters0 = check_sub_authzs(SubPkt, TopicFilters1, Channel), HasAuthzDeny = lists:any( fun({_TopicFilter, ReasonCode}) -> ReasonCode =:= ?RC_NOT_AUTHORIZED @@ -1838,14 +1839,34 @@ check_pub_alias( check_pub_alias(_Packet, _Channel) -> 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_authz( - #mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, + #mqtt_packet{ + variable = #mqtt_packet_publish{topic_name = Topic} + } = Packet, #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; deny -> {error, ?RC_NOT_AUTHORIZED} end. @@ -1868,24 +1889,23 @@ check_pub_caps( %%-------------------------------------------------------------------- %% Check Sub Authorization -%% TODO: not only check topic filter. Qos chould be checked too. -%% Not implemented yet: -%% MQTT-3.1.1 [MQTT-3.8.4-6] and MQTT-5.0 [MQTT-3.8.4-7] -check_sub_authzs(TopicFilters, Channel) -> - check_sub_authzs(TopicFilters, Channel, []). +check_sub_authzs(Packet, TopicFilters, Channel) -> + Action = authz_action(Packet), + check_sub_authzs(Action, TopicFilters, Channel, []). check_sub_authzs( + Action, [TopicFilter = {Topic, _} | More], Channel = #channel{clientinfo = ClientInfo}, Acc ) -> - case emqx_access_control:authorize(ClientInfo, subscribe, Topic) of + case emqx_access_control:authorize(ClientInfo, Action, Topic) of allow -> - check_sub_authzs(More, Channel, [{TopicFilter, ?RC_SUCCESS} | Acc]); + check_sub_authzs(Action, More, Channel, [{TopicFilter, ?RC_SUCCESS} | Acc]); deny -> - check_sub_authzs(More, Channel, [{TopicFilter, ?RC_NOT_AUTHORIZED} | Acc]) + check_sub_authzs(Action, More, Channel, [{TopicFilter, ?RC_NOT_AUTHORIZED} | Acc]) end; -check_sub_authzs([], _Channel, Acc) -> +check_sub_authzs(_Action, [], _Channel, Acc) -> lists:reverse(Acc). %%-------------------------------------------------------------------- @@ -2149,7 +2169,8 @@ publish_will_msg( ClientInfo = #{mountpoint := MountPoint}, 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), case PublishingDisallowed orelse ClientBanned of true -> diff --git a/apps/emqx/src/emqx_types.erl b/apps/emqx/src/emqx_types.erl index c6a567318..cc937f81c 100644 --- a/apps/emqx/src/emqx_types.erl +++ b/apps/emqx/src/emqx_types.erl @@ -29,6 +29,7 @@ -export_type([ zone/0, pubsub/0, + pubsub_action/0, subid/0 ]). @@ -127,7 +128,12 @@ | exactly_once. -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 group() :: binary() | undefined. diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index 305eaf5eb..5d4344de6 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/apps/emqx/test/emqx_access_control_SUITE.erl @@ -19,8 +19,8 @@ -compile(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_access_control.hrl"). -include_lib("eunit/include/eunit.hrl"). all() -> emqx_common_test_helpers:all(?MODULE). @@ -44,8 +44,7 @@ t_authenticate(_) -> ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())). t_authorize(_) -> - Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>), - ?assertEqual(allow, emqx_access_control:authorize(clientinfo(), Publish, <<"t">>)). + ?assertEqual(allow, emqx_access_control:authorize(clientinfo(), ?AUTHZ_PUBLISH, <<"t">>)). t_delayed_authorize(_) -> RawTopic = <<"$delayed/1/foo/2">>, @@ -54,11 +53,11 @@ t_delayed_authorize(_) -> 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(), Publish1, RawTopic)), + ?assertEqual(allow, emqx_access_control:authorize(clientinfo(), ?AUTHZ_PUBLISH, RawTopic)), - Publish2 = ?PUBLISH_PACKET(?QOS_0, InvalidTopic, 1, <<"payload">>), - ?assertEqual(deny, emqx_access_control:authorize(clientinfo(), Publish2, InvalidTopic)), + ?assertEqual( + deny, emqx_access_control:authorize(clientinfo(), ?AUTHZ_PUBLISH, InvalidTopic) + ), ok. t_quick_deny_anonymous(_) -> @@ -96,8 +95,8 @@ t_quick_deny_anonymous(_) -> %% Helper functions %%-------------------------------------------------------------------- -authz_stub(_Client, _PubSub, ValidTopic, _DefaultResult, ValidTopic) -> {stop, #{result => allow}}; -authz_stub(_Client, _PubSub, _Topic, _DefaultResult, _ValidTopic) -> {stop, #{result => deny}}. +authz_stub(_Client, _Action, ValidTopic, _DefaultResult, ValidTopic) -> {stop, #{result => allow}}; +authz_stub(_Client, _Action, _Topic, _DefaultResult, _ValidTopic) -> {stop, #{result => deny}}. quick_deny_anonymous_authn(#{username := <<"badname">>}, _AuthResult) -> {stop, {error, not_authorized}}; diff --git a/apps/emqx/test/emqx_authz_cache_SUITE.erl b/apps/emqx/test/emqx_authz_cache_SUITE.erl index 5497422af..09d1e1522 100644 --- a/apps/emqx/test/emqx_authz_cache_SUITE.erl +++ b/apps/emqx/test/emqx_authz_cache_SUITE.erl @@ -43,8 +43,6 @@ t_clean_authz_cache(_) -> ct:sleep(100), ClientPid = case emqx_cm:lookup_channels(<<"emqx_c">>) of - [Pid] when is_pid(Pid) -> - Pid; Pids when is_list(Pids) -> lists:last(Pids); _ -> diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 5653cd2d2..f266dbcfa 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -908,7 +908,8 @@ t_check_pub_alias(_) -> t_check_sub_authzs(_) -> emqx_config:put_zone_conf(default, [authorization, enable], true), 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(_) -> ok = meck:new(emqx_mqtt_caps, [passthrough, no_history]), diff --git a/apps/emqx/test/emqx_proper_types.erl b/apps/emqx/test/emqx_proper_types.erl index 95cf29bee..6d1ced486 100644 --- a/apps/emqx/test/emqx_proper_types.erl +++ b/apps/emqx/test/emqx_proper_types.erl @@ -20,6 +20,7 @@ -include_lib("proper/include/proper.hrl"). -include("emqx.hrl"). +-include("emqx_access_control.hrl"). %% High level Types -export([ @@ -34,7 +35,8 @@ subopts/0, nodename/0, normal_topic/0, - normal_topic_filter/0 + normal_topic_filter/0, + pubsub/0 ]). %% Basic Types @@ -482,6 +484,23 @@ normal_topic_filter() -> 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 %%-------------------------------------------------------------------- diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index b43a2cdab..8676da134 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -18,16 +18,6 @@ -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 -define(ACL_TABLE, emqx_acl). @@ -72,6 +62,20 @@ topic => <<"eq test/#">>, permission => <<"deny">>, 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/#">>, permission => <<"deny">>, 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/#">>, permission => <<"deny">>, 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, #{ page => 1, limit => 100, @@ -121,3 +158,8 @@ }). -define(RESOURCE_GROUP, <<"emqx_authz">>). + +-define(AUTHZ_FEATURES, [rich_actions]). + +-define(DEFAULT_RULE_QOS, [0, 1, 2]). +-define(DEFAULT_RULE_RETAIN, all). diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 6f45a88b7..b5f1a1298 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -39,6 +39,11 @@ get_enabled_authzs/0 ]). +-export([ + feature_available/1, + set_feature_available/2 +]). + -export([post_config_update/5, pre_config_update/3]). -export([acl_conf_file/0]). @@ -519,6 +524,28 @@ read_acl_file(#{<<"path">> := Path} = Source) -> {ok, Rules} = emqx_authz_file:read_file(Path), 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 %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authz/src/emqx_authz_api_mnesia.erl b/apps/emqx_authz/src/emqx_authz_api_mnesia.erl index f5ac40f5e..a2a8f2525 100644 --- a/apps/emqx_authz/src/emqx_authz_api_mnesia.erl +++ b/apps/emqx_authz/src/emqx_authz_api_mnesia.erl @@ -359,6 +359,22 @@ fields(rule_item) -> required => true, 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) -> @@ -434,7 +450,7 @@ users(post, #{body := Body}) when is_list(Body) -> [] -> lists:foreach( fun(#{<<"username">> := Username, <<"rules">> := Rules}) -> - emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)) + emqx_authz_mnesia:store_rules({username, Username}, Rules) end, Body ), @@ -470,7 +486,7 @@ clients(post, #{body := Body}) when is_list(Body) -> [] -> lists:foreach( fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) -> - emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules)) + emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules) end, Body ), @@ -489,21 +505,14 @@ user(get, #{bindings := #{username := Username}}) -> {ok, Rules} -> {200, #{ username => Username, - rules => [ - #{ - topic => Topic, - action => Action, - permission => Permission - } - || {Permission, Action, Topic} <- Rules - ] + rules => format_rules(Rules) }} end; user(put, #{ bindings := #{username := Username}, body := #{<<"username">> := Username, <<"rules">> := Rules} }) -> - emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)), + emqx_authz_mnesia:store_rules({username, Username}, Rules), {204}; user(delete, #{bindings := #{username := Username}}) -> case emqx_authz_mnesia:get_rules({username, Username}) of @@ -521,21 +530,14 @@ client(get, #{bindings := #{clientid := ClientID}}) -> {ok, Rules} -> {200, #{ clientid => ClientID, - rules => [ - #{ - topic => Topic, - action => Action, - permission => Permission - } - || {Permission, Action, Topic} <- Rules - ] + rules => format_rules(Rules) }} end; client(put, #{ bindings := #{clientid := ClientID}, body := #{<<"clientid">> := ClientID, <<"rules">> := Rules} }) -> - emqx_authz_mnesia:store_rules({clientid, ClientID}, format_rules(Rules)), + emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules), {204}; client(delete, #{bindings := #{clientid := ClientID}}) -> case emqx_authz_mnesia:get_rules({clientid, ClientID}) of @@ -552,18 +554,11 @@ all(get, _) -> {200, #{rules => []}}; {ok, Rules} -> {200, #{ - rules => [ - #{ - topic => Topic, - action => Action, - permission => Permission - } - || {Permission, Action, Topic} <- Rules - ] + rules => format_rules(Rules) }} end; all(post, #{body := #{<<"rules">> := Rules}}) -> - emqx_authz_mnesia:store_rules(all, format_rules(Rules)), + emqx_authz_mnesia:store_rules(all, Rules), {204}; all(delete, _) -> emqx_authz_mnesia:store_rules(all, []), @@ -626,58 +621,20 @@ run_fuzzy_filter( %%-------------------------------------------------------------------- %% 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([{username, Username}, {rules, Rules}]) -> #{ username => Username, - rules => [ - #{ - topic => Topic, - action => Action, - permission => Permission - } - || {Permission, Action, Topic} <- Rules - ] + rules => format_rules(Rules) }; format_result([{clientid, ClientID}, {rules, Rules}]) -> #{ clientid => ClientID, - rules => [ - #{ - topic => Topic, - action => Action, - permission => Permission - } - || {Permission, Action, Topic} <- Rules - ] + rules => format_rules(Rules) }. -atom(B) when is_binary(B) -> - try - binary_to_existing_atom(B, utf8) - catch - _Error:_Expection -> binary_to_atom(B) - end; -atom(A) when is_atom(A) -> A. + +format_rules(Rules) -> + [emqx_authz_rule_raw:format_rule(Rule) || Rule <- Rules]. %%-------------------------------------------------------------------- %% Internal functions diff --git a/apps/emqx_authz/src/emqx_authz_file.erl b/apps/emqx_authz/src/emqx_authz_file.erl index 317395a45..7d421d39b 100644 --- a/apps/emqx_authz/src/emqx_authz_file.erl +++ b/apps/emqx_authz/src/emqx_authz_file.erl @@ -16,7 +16,6 @@ -module(emqx_authz_file). --include("emqx_authz.hrl"). -include_lib("emqx/include/logger.hrl"). -behaviour(emqx_authz). diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index 5747e6eeb..aafbe25ad 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -51,6 +51,11 @@ ?PH_CERT_CN_NAME ]). +-define(PLACEHOLDERS_FOR_RICH_ACTIONS, [ + ?PH_QOS, + ?PH_RETAIN +]). + description() -> "AuthZ with http". @@ -72,7 +77,7 @@ destroy(#{annotations := #{id := Id}}) -> authorize( Client, - PubSub, + Action, Topic, #{ type := http, @@ -81,7 +86,7 @@ authorize( request_timeout := RequestTimeout } = 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 {ok, 204, _Headers} -> {matched, allow}; @@ -139,14 +144,14 @@ parse_config( method => Method, base_url => BaseUrl, 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( cow_qs:parse_qs(to_bin(Query)), - ?PLACEHOLDERS + placeholders() ), body_template => emqx_authz_utils:parse_deep( maps:to_list(maps:get(body, Conf, #{})), - ?PLACEHOLDERS + placeholders() ), request_timeout => ReqTimeout, %% pool_type default value `random` @@ -173,7 +178,7 @@ parse_url(Url) -> end. generate_request( - PubSub, + Action, Topic, Client, #{ @@ -184,7 +189,7 @@ generate_request( body_template := BodyTemplate } ) -> - Values = client_vars(Client, PubSub, Topic), + Values = client_vars(Client, Action, Topic), Path = emqx_authz_utils:render_urlencoded_str(BasePathTemplate, Values), Query = emqx_authz_utils:render_deep(BaseQueryTemplate, 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) -> query_string(Body). -client_vars(Client, PubSub, Topic) -> - Client#{ - action => PubSub, - topic => Topic - }. +client_vars(Client, Action, Topic) -> + Vars = emqx_authz_utils:vars_for_rule_query(Client, Action), + Vars#{topic => Topic}. to_list(A) when is_atom(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(L) when is_list(L) -> list_to_binary(L); to_bin(X) -> X. + +placeholders() -> + placeholders(emqx_authz:feature_available(rich_actions)). + +placeholders(true) -> + ?PLACEHOLDERS ++ ?PLACEHOLDERS_FOR_RICH_ACTIONS; +placeholders(false) -> + ?PLACEHOLDERS. diff --git a/apps/emqx_authz/src/emqx_authz_mnesia.erl b/apps/emqx_authz/src/emqx_authz_mnesia.erl index bdb4877c0..2cecd0c71 100644 --- a/apps/emqx_authz/src/emqx_authz_mnesia.erl +++ b/apps/emqx_authz/src/emqx_authz_mnesia.erl @@ -16,7 +16,6 @@ -module(emqx_authz_mnesia). --include_lib("emqx/include/emqx.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("emqx/include/logger.hrl"). @@ -202,25 +201,16 @@ record_count() -> %%-------------------------------------------------------------------- normalize_rules(Rules) -> - lists:map(fun normalize_rule/1, Rules). + lists:flatmap(fun normalize_rule/1, Rules). -normalize_rule({Permission, Action, Topic}) -> - {normalize_permission(Permission), normalize_action(Action), normalize_topic(Topic)}; -normalize_rule(Rule) -> - error({invalid_rule, Rule}). - -normalize_topic(Topic) when is_list(Topic) -> list_to_binary(Topic); -normalize_topic(Topic) when is_binary(Topic) -> Topic; -normalize_topic(Topic) -> error({invalid_rule_topic, Topic}). - -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}). +normalize_rule(RuleRaw) -> + case emqx_authz_rule_raw:parse_rule(RuleRaw) of + %% For backward compatibility + {ok, {Permission, Action, TopicFilters}} -> + [{Permission, Action, TopicFilter} || TopicFilter <- TopicFilters]; + {error, Reason} -> + error(Reason) + end. do_get_rules(Key) -> case mnesia:dirty_read(?ACL_TABLE, Key) of diff --git a/apps/emqx_authz/src/emqx_authz_mongodb.erl b/apps/emqx_authz/src/emqx_authz_mongodb.erl index e82ff64e1..7adb6d2d9 100644 --- a/apps/emqx_authz/src/emqx_authz_mongodb.erl +++ b/apps/emqx_authz/src/emqx_authz_mongodb.erl @@ -68,7 +68,7 @@ destroy(#{annotations := #{id := Id}}) -> authorize( Client, - PubSub, + Action, Topic, #{ collection := Collection, @@ -97,15 +97,21 @@ authorize( {ok, []} -> nomatch; {ok, Rows} -> - Rules = [ - emqx_authz_rule:compile({Permission, all, Action, Topics}) - || #{ - <<"topics">> := Topics, - <<"permission">> := Permission, - <<"action">> := Action - } <- Rows - ], - do_authorize(Client, PubSub, Topic, Rules) + Rules = lists:flatmap(fun parse_rule/1, Rows), + do_authorize(Client, Action, Topic, Rules) + end. + +parse_rule(Row) -> + case emqx_authz_rule_raw:parse_rule(Row) of + {ok, {Permission, Action, Topics}} -> + [emqx_authz_rule:compile({Permission, all, Action, Topics})]; + {error, Reason} -> + ?SLOG(error, #{ + msg => "parse_rule_error", + reason => Reason, + row => Row + }), + [] end. do_authorize(_Client, _PubSub, _Topic, []) -> diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index fb6a29c3d..01debaea9 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -55,7 +55,7 @@ create(#{query := SQL} = Source0) -> ResourceId = emqx_authz_utils:make_resource_id(?MODULE), Source = Source0#{prepare_statement => #{?PREPARE_KEY => PrepareSQL}}, {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) -> {PrepareSQL, TmplToken} = emqx_authz_utils:parse_sql(SQL, '?', ?PLACEHOLDERS), @@ -64,7 +64,7 @@ update(#{query := SQL} = Source0) -> {error, Reason} -> error({load_config_error, Reason}); {ok, Id} -> - Source#{annotations => #{id => Id, tmpl_oken => TmplToken}} + Source#{annotations => #{id => Id, tmpl_token => TmplToken}} end. destroy(#{annotations := #{id := Id}}) -> @@ -72,57 +72,51 @@ destroy(#{annotations := #{id := Id}}) -> authorize( Client, - PubSub, + Action, Topic, #{ annotations := #{ 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 emqx_resource:simple_sync_query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams}) of - {ok, _Columns, []} -> + {ok, _ColumnNames, []} -> nomatch; - {ok, Columns, Rows} -> - do_authorize(Client, PubSub, Topic, Columns, Rows); + {ok, ColumnNames, Rows} -> + do_authorize(Client, Action, Topic, ColumnNames, Rows); {error, Reason} -> ?SLOG(error, #{ msg => "query_mysql_error", reason => Reason, - tmpl_oken => TmplToken, + tmpl_token => TmplToken, params => RenderParams, resource_id => ResourceID }), nomatch end. -do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> +do_authorize(_Client, _Action, _Topic, _ColumnNames, []) -> nomatch; -do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> - case +do_authorize(Client, Action, Topic, ColumnNames, [Row | Tail]) -> + try emqx_authz_rule:match( - Client, - PubSub, - Topic, - emqx_authz_rule:compile(format_result(Columns, Row)) + Client, Action, Topic, emqx_authz_utils:parse_rule_from_row(ColumnNames, Row) ) of {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. - -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). diff --git a/apps/emqx_authz/src/emqx_authz_postgresql.erl b/apps/emqx_authz/src/emqx_authz_postgresql.erl index 05f2315a6..1b05451cc 100644 --- a/apps/emqx_authz/src/emqx_authz_postgresql.erl +++ b/apps/emqx_authz/src/emqx_authz_postgresql.erl @@ -21,6 +21,8 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-include_lib("epgsql/include/epgsql.hrl"). + -behaviour(emqx_authz). %% AuthZ Callbacks @@ -77,7 +79,7 @@ destroy(#{annotations := #{id := Id}}) -> authorize( Client, - PubSub, + Action, Topic, #{ 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 emqx_resource:simple_sync_query(ResourceID, {prepared_query, ResourceID, RenderedParams}) of {ok, _Columns, []} -> nomatch; {ok, Columns, Rows} -> - do_authorize(Client, PubSub, Topic, Columns, Rows); + do_authorize(Client, Action, Topic, column_names(Columns), Rows); {error, Reason} -> ?SLOG(error, #{ msg => "query_postgresql_error", @@ -104,33 +107,29 @@ authorize( nomatch end. -do_authorize(_Client, _PubSub, _Topic, _Columns, []) -> +do_authorize(_Client, _Action, _Topic, _ColumnNames, []) -> nomatch; -do_authorize(Client, PubSub, Topic, Columns, [Row | Tail]) -> - case +do_authorize(Client, Action, Topic, ColumnNames, [Row | Tail]) -> + try emqx_authz_rule:match( - Client, - PubSub, - Topic, - emqx_authz_rule:compile(format_result(Columns, Row)) + Client, Action, Topic, emqx_authz_utils:parse_rule_from_row(ColumnNames, Row) ) of {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. -format_result(Columns, Row) -> - Permission = lists:nth(index(<<"permission">>, 2, Columns), erlang:tuple_to_list(Row)), - Action = lists:nth(index(<<"action">>, 2, Columns), erlang:tuple_to_list(Row)), - Topic = lists:nth(index(<<"topic">>, 2, Columns), erlang:tuple_to_list(Row)), - {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). +column_names(Columns) -> + lists:map( + fun(#column{name = Name}) -> Name end, + Columns + ). diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 3b19db832..01149b5bb 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -70,19 +70,20 @@ destroy(#{annotations := #{id := Id}}) -> authorize( Client, - PubSub, + Action, Topic, #{ cmd_template := CmdTemplate, 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 {ok, []} -> nomatch; {ok, Rows} -> - do_authorize(Client, PubSub, Topic, Rows); + do_authorize(Client, Action, Topic, Rows); {error, Reason} -> ?SLOG(error, #{ msg => "query_redis_error", @@ -93,21 +94,60 @@ authorize( nomatch end. -do_authorize(_Client, _PubSub, _Topic, []) -> +do_authorize(_Client, _Action, _Topic, []) -> nomatch; -do_authorize(Client, PubSub, Topic, [TopicFilter, Action | Tail]) -> - case +do_authorize(Client, Action, Topic, [TopicFilterRaw, RuleEncoded | Tail]) -> + try emqx_authz_rule:match( Client, - PubSub, + Action, Topic, - emqx_authz_rule:compile({allow, all, Action, [TopicFilter]}) + compile_rule(RuleEncoded, TopicFilterRaw) ) of {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. tokens(Query) -> Tokens = binary:split(Query, <<" ">>, [global]), [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. diff --git a/apps/emqx_authz/src/emqx_authz_rule.erl b/apps/emqx_authz/src/emqx_authz_rule.erl index ec1a8c5de..a59679a8d 100644 --- a/apps/emqx_authz/src/emqx_authz_rule.erl +++ b/apps/emqx_authz/src/emqx_authz_rule.erl @@ -16,9 +16,9 @@ -module(emqx_authz_rule). --include("emqx_authz.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-include("emqx_authz.hrl"). -ifdef(TEST). -compile(export_all). @@ -32,50 +32,123 @@ compile/1 ]). --type ipaddress() :: - {ipaddr, esockd_cidr:cidr_string()} - | {ipaddrs, list(esockd_cidr:cidr_string())}. +-type permission() :: allow | deny. --type username() :: {username, binary()}. - --type clientid() :: {clientid, binary()}. - --type who() :: +-type who_condition() :: ipaddress() | username() | clientid() | {'and', [ipaddress() | username() | clientid()]} | {'or', [ipaddress() | username() | clientid()]} | 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 permission() :: allow | deny. +-type action_condition() :: + 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([ - 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 - ?ALLOW_DENY(Permission) + ?IS_PERMISSION(Permission) -> {Permission, all, all, [compile_topic(<<"#">>)]}; 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) || 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}); -compile({_Permission, _Who, Action, _TopicFilter}) when not ?PUBSUB(Action) -> - throw({invalid_authorization_action, Action}); compile(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) -> all; compile_who({user, Username}) -> @@ -99,8 +172,12 @@ compile_who({ipaddrs, CIDRs}) -> compile_who({'and', L}) when is_list(L) -> {'and', [compile_who(Who) || Who <- L]}; compile_who({'or', L}) when is_list(L) -> - {'or', [compile_who(Who) || Who <- L]}. + {'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>>) -> {eq, emqx_topic:words(Topic)}; compile_topic({eq, Topic}) -> @@ -117,45 +194,65 @@ compile_topic(Topic) -> Tokens -> {pattern, Tokens} end. -atom(B) when is_binary(B) -> - try - binary_to_existing_atom(B, utf8) - catch - _E:_S -> binary_to_atom(B) - end; -atom(A) when is_atom(A) -> A. - bin(L) when is_list(L) -> list_to_binary(L); bin(B) when is_binary(B) -> B. --spec matches(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), [rule()]) -> +-spec matches(emqx_types:clientinfo(), action(), emqx_types:topic(), [rule()]) -> {matched, allow} | {matched, deny} | nomatch. -matches(_Client, _PubSub, _Topic, []) -> +matches(_Client, _Action, _Topic, []) -> nomatch; -matches(Client, PubSub, Topic, [{Permission, Who, Action, TopicFilters} | Tail]) -> - case match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) of - nomatch -> matches(Client, PubSub, Topic, Tail); +matches(Client, Action, Topic, [{Permission, WhoCond, ActionCond, TopicCond} | Tail]) -> + case match(Client, Action, Topic, {Permission, WhoCond, ActionCond, TopicCond}) of + nomatch -> matches(Client, Action, Topic, Tail); Matched -> Matched 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. -match(Client, PubSub, Topic, {Permission, Who, Action, TopicFilters}) -> +match(Client, Action, Topic, {Permission, WhoCond, ActionCond, TopicCond}) -> case - match_action(PubSub, Action) andalso - match_who(Client, Who) andalso - match_topics(Client, Topic, TopicFilters) + match_action(Action, ActionCond) andalso + match_who(Client, WhoCond) andalso + match_topics(Client, Topic, TopicCond) of true -> {matched, Permission}; _ -> nomatch end. -match_action(publish, publish) -> true; -match_action(subscribe, subscribe) -> true; -match_action(_, all) -> true; -match_action(_, _) -> false. +-spec match_action(action(), action_condition()) -> boolean(). +match_action(#{action_type := publish}, PubSubCond) when is_atom(PubSubCond) -> + match_pubsub(publish, PubSubCond); +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) -> true; diff --git a/apps/emqx_authz/src/emqx_authz_rule_raw.erl b/apps/emqx_authz/src/emqx_authz_rule_raw.erl new file mode 100644 index 000000000..1fbe2ca45 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_rule_raw.erl @@ -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. diff --git a/apps/emqx_authz/src/emqx_authz_utils.erl b/apps/emqx_authz/src/emqx_authz_utils.erl index ec112070e..d903ae027 100644 --- a/apps/emqx_authz/src/emqx_authz_utils.erl +++ b/apps/emqx_authz/src/emqx_authz_utils.erl @@ -31,7 +31,10 @@ parse_sql/3, render_deep/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([ @@ -43,6 +46,8 @@ start_after_created => false }). +-include_lib("emqx/include/logger.hrl"). + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- @@ -171,6 +176,24 @@ content_type(Headers) when is_list(Headers) -> <<"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 %%-------------------------------------------------------------------- @@ -208,3 +231,8 @@ handle_sql_var(_Name, Value) -> bin(A) when is_atom(A) -> atom_to_binary(A, utf8); bin(L) when is_list(L) -> list_to_binary(L); bin(X) -> X. + +to_list(Tuple) when is_tuple(Tuple) -> + tuple_to_list(Tuple); +to_list(List) when is_list(List) -> + List. diff --git a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl index 3775b9a1c..7f03a38a2 100644 --- a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl @@ -96,7 +96,7 @@ t_api(_) -> <<"hasnext">> := false } } = emqx_utils_json:decode(Request1), - ?assertEqual(3, length(Rules1)), + ?assertEqual(?USERNAME_RULES_EXAMPLE_COUNT, length(Rules1)), {ok, 200, Request1_1} = request( @@ -204,7 +204,7 @@ t_api(_) -> } = emqx_utils_json:decode(Request4), #{<<"clientid">> := <<"client1">>, <<"rules">> := Rules3} = emqx_utils_json:decode(Request5), - ?assertEqual(3, length(Rules3)), + ?assertEqual(?CLIENTID_RULES_EXAMPLE_COUNT, length(Rules3)), {ok, 204, _} = request( @@ -253,7 +253,7 @@ t_api(_) -> [] ), #{<<"rules">> := Rules5} = emqx_utils_json:decode(Request7), - ?assertEqual(3, length(Rules5)), + ?assertEqual(?ALL_RULES_EXAMPLE_COUNT, length(Rules5)), {ok, 204, _} = request( diff --git a/apps/emqx_authz/test/emqx_authz_file_SUITE.erl b/apps/emqx_authz/test/emqx_authz_file_SUITE.erl index ec96522a5..0ce788f8d 100644 --- a/apps/emqx_authz/test/emqx_authz_file_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_file_SUITE.erl @@ -42,11 +42,11 @@ init_per_suite(Config) -> Config. end_per_suite(_Config) -> - ok. + ok = emqx_authz_test_lib:restore_authorizers(). init_per_testcase(TestCase, Config) -> 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)} ), [{tc_apps, Apps} | Config]. @@ -59,13 +59,7 @@ end_per_testcase(_TestCase, Config) -> %%------------------------------------------------------------------------------ t_ok(_Config) -> - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, + ClientInfo = emqx_authz_test_lib:base_client_info(), ok = setup_config(?RAW_SOURCE#{ <<"rules">> => <<"{allow, {user, \"username\"}, publish, [\"t\"]}.">> @@ -73,23 +67,52 @@ t_ok(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ), ?assertEqual( 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) -> - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - is_superuser => true, - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, + ClientInfo = + emqx_authz_test_lib:client_info(#{is_superuser => true}), %% no rules apply to superuser ok = setup_config(?RAW_SOURCE#{ @@ -98,12 +121,12 @@ t_superuser(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ), ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, subscribe, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_SUBSCRIBE, <<"t">>) ). t_invalid_file(_Config) -> diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index 702bf2756..6cf4b5bc0 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -65,6 +65,7 @@ init_per_testcase(_Case, Config) -> Config. end_per_testcase(_Case, _Config) -> + _ = emqx_authz:set_feature_available(rich_actions, true), try ok = emqx_authz_http_test_server:stop() catch @@ -97,7 +98,7 @@ t_response_handling(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ), %% 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 = setup_handler_and_config( @@ -122,7 +123,7 @@ t_response_handling(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ), %% Not OK, get, 400 @@ -136,7 +137,7 @@ t_response_handling(_Config) -> ?assertEqual( deny, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ), %% Not OK, get, 400 + body & headers @@ -155,7 +156,7 @@ t_response_handling(_Config) -> ?assertEqual( 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 @@ -165,7 +166,7 @@ t_response_handling(_Config) -> ?check_trace( ?assertEqual( deny, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ), fun(Trace) -> ?assertMatch( @@ -200,7 +201,9 @@ t_query_params(_Config) -> proto_name := <<"MQTT">>, mountpoint := <<"MOUNTPOINT">>, topic := <<"t/1">>, - action := <<"publish">> + action := <<"publish">>, + qos := <<"1">>, + retain := <<"false">> } = cowboy_req:match_qs( [ username, @@ -209,7 +212,9 @@ t_query_params(_Config) -> proto_name, mountpoint, topic, - action + action, + qos, + retain ], Req0 ), @@ -224,7 +229,9 @@ t_query_params(_Config) -> "proto_name=${proto_name}&" "mountpoint=${mountpoint}&" "topic=${topic}&" - "action=${action}" + "action=${action}&" + "qos=${qos}&" + "retain=${retain}" >> } ), @@ -241,7 +248,7 @@ t_query_params(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t/1">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t/1">>) ). t_path(_Config) -> @@ -256,7 +263,9 @@ t_path(_Config) -> "MQTT/" "MOUNTPOINT/" "t%2F1/" - "publish" + "publish/" + "1/" + "false" >>, cowboy_req:path(Req0) ), @@ -271,7 +280,9 @@ t_path(_Config) -> "${proto_name}/" "${mountpoint}/" "${topic}/" - "${action}" + "${action}/" + "${qos}/" + "${retain}" >> } ), @@ -288,7 +299,7 @@ t_path(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t/1">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t/1">>) ). t_json_body(_Config) -> @@ -309,7 +320,9 @@ t_json_body(_Config) -> <<"proto_name">> := <<"MQTT">>, <<"mountpoint">> := <<"MOUNTPOINT">>, <<"topic">> := <<"t">>, - <<"action">> := <<"publish">> + <<"action">> := <<"publish">>, + <<"qos">> := <<"1">>, + <<"retain">> := <<"false">> }, emqx_utils_json:decode(RawBody, [return_maps]) ), @@ -324,7 +337,9 @@ t_json_body(_Config) -> <<"proto_name">> => <<"${proto_name}">>, <<"mountpoint">> => <<"${mountpoint}">>, <<"topic">> => <<"${topic}">>, - <<"action">> => <<"${action}">> + <<"action">> => <<"${action}">>, + <<"qos">> => <<"${qos}">>, + <<"retain">> => <<"${retain}">> } } ), @@ -341,7 +356,45 @@ t_json_body(_Config) -> ?assertEqual( 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) -> @@ -401,7 +454,7 @@ t_placeholder_and_body(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ). t_no_value_for_placeholder(_Config) -> @@ -441,7 +494,7 @@ t_no_value_for_placeholder(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ). t_create_replace(_Config) -> @@ -466,7 +519,7 @@ t_create_replace(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ), %% Changing to valid config @@ -485,7 +538,7 @@ t_create_replace(_Config) -> ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ). %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authz/test/emqx_authz_jwt_SUITE.erl b/apps/emqx_authz/test/emqx_authz_jwt_SUITE.erl index 56e893c5b..fcaa378c5 100644 --- a/apps/emqx_authz/test/emqx_authz_jwt_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_jwt_SUITE.erl @@ -22,6 +22,7 @@ -include_lib("emqx/include/emqx_placeholder.hrl"). -include_lib("emqx_authn/include/emqx_authn.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("emqx/include/emqx_access_control.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -341,12 +342,12 @@ t_check_undefined_expire(_Config) -> ?assertMatch( {matched, allow}, - emqx_authz_client_info:authorize(Client, subscribe, <<"a/b">>, undefined) + emqx_authz_client_info:authorize(Client, ?AUTHZ_SUBSCRIBE, <<"a/b">>, undefined) ), ?assertMatch( {matched, deny}, - emqx_authz_client_info:authorize(Client, subscribe, <<"a/bar">>, undefined) + emqx_authz_client_info:authorize(Client, ?AUTHZ_SUBSCRIBE, <<"a/bar">>, undefined) ). %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authz/test/emqx_authz_mnesia_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mnesia_SUITE.erl index 2b7fce309..c82bbd56e 100644 --- a/apps/emqx_authz/test/emqx_authz_mnesia_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mnesia_SUITE.erl @@ -18,6 +18,8 @@ -compile(nowarn_export_all). -compile(export_all). +-include_lib("emqx_authz.hrl"). + -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -44,6 +46,7 @@ init_per_testcase(_TestCase, Config) -> Config. end_per_testcase(_TestCase, _Config) -> + _ = emqx_authz:set_feature_available(rich_actions, true), ok = emqx_authz_mnesia:purge_rules(). set_special_configs(emqx_authz) -> @@ -54,51 +57,135 @@ set_special_configs(_) -> %%------------------------------------------------------------------------------ %% Testcases %%------------------------------------------------------------------------------ -t_username_topic_rules(_Config) -> - ok = test_topic_rules(username). -t_clientid_topic_rules(_Config) -> - ok = test_topic_rules(clientid). +t_authz(_Config) -> + ClientInfo = emqx_authz_test_lib:base_client_info(), -t_all_topic_rules(_Config) -> - ok = test_topic_rules(all). + test_authz( + 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) -> - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, +test_authz(Expected, ExpectedNoRichActions, {Who, Rule}, {ClientInfo, Action, Topic}) -> + test_authz_with_rich_actions(true, Expected, {Who, Rule}, {ClientInfo, Action, Topic}), + test_authz_with_rich_actions( + false, ExpectedNoRichActions, {Who, Rule}, {ClientInfo, Action, Topic} + ). - SetupSamples = fun(CInfo, Samples) -> - setup_client_samples(CInfo, Samples, Key) - end, - - ok = emqx_authz_test_lib:test_no_topic_rules(ClientInfo, SetupSamples), - - ok = emqx_authz_test_lib:test_allow_topic_rules(ClientInfo, SetupSamples), - - ok = emqx_authz_test_lib:test_deny_topic_rules(ClientInfo, SetupSamples). +test_authz_with_rich_actions( + RichActionsEnabled, Expected, {Who, Rule}, {ClientInfo, Action, Topic} +) -> + ct:pal("Test authz rich_actions:~p~nwho:~p~nrule:~p~nattempt:~p~nexpected ~p", [ + RichActionsEnabled, Who, Rule, {ClientInfo, Action, Topic}, Expected + ]), + try + _ = emqx_authz:set_feature_available(rich_actions, RichActionsEnabled), + 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) -> - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, + ClientInfo = emqx_authz_test_lib:base_client_info(), ok = emqx_authz_mnesia:store_rules( {username, <<"username">>}, - [{allow, publish, "t"}] + [#{<<"permission">> => <<"allow">>, <<"action">> => <<"publish">>, <<"topic">> => <<"t">>}] ), ?assertEqual( allow, - emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>) ), ?assertException( @@ -106,25 +193,31 @@ t_normalize_rules(_Config) -> {invalid_rule, _}, emqx_authz_mnesia:store_rules( {username, <<"username">>}, - [[allow, publish, <<"t">>]] + [[<<"allow">>, <<"publish">>, <<"t">>]] ) ), ?assertException( error, - {invalid_rule_action, _}, + {invalid_action, _}, emqx_authz_mnesia:store_rules( {username, <<"username">>}, - [{allow, pub, <<"t">>}] + [#{<<"permission">> => <<"allow">>, <<"action">> => <<"pub">>, <<"topic">> => <<"t">>}] ) ), ?assertException( error, - {invalid_rule_permission, _}, + {invalid_permission, _}, emqx_authz_mnesia:store_rules( {username, <<"username">>}, - [{accept, publish, <<"t">>}] + [ + #{ + <<"permission">> => <<"accept">>, + <<"action">> => <<"publish">>, + <<"topic">> => <<"t">> + } + ] ) ). @@ -138,27 +231,5 @@ raw_mnesia_authz_config() -> <<"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() -> emqx_authz_test_lib:setup_config(raw_mnesia_authz_config(), #{}). diff --git a/apps/emqx_authz/test/emqx_authz_mongodb_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongodb_SUITE.erl index 9ffeacf45..4476deda2 100644 --- a/apps/emqx_authz/test/emqx_authz_mongodb_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongodb_SUITE.erl @@ -28,10 +28,10 @@ -define(MONGO_CLIENT, 'emqx_authz_mongo_SUITE_client'). all() -> - emqx_common_test_helpers:all(?MODULE). + emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()). groups() -> - []. + emqx_authz_test_lib:table_groups(t_run_case, cases()). init_per_suite(Config) -> ok = stop_apps([emqx_resource]), @@ -57,12 +57,18 @@ set_special_configs(emqx_authz) -> set_special_configs(_) -> 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) -> {ok, _} = mc_worker_api:connect(mongo_config()), ok = emqx_authz_test_lib:reset_authorizers(), Config. end_per_testcase(_TestCase, _Config) -> + _ = emqx_authz:set_feature_available(rich_actions, true), ok = reset_samples(), ok = mc_worker_api:disconnect(?MONGO_CLIENT). @@ -70,233 +76,313 @@ end_per_testcase(_TestCase, _Config) -> %% Testcases %%------------------------------------------------------------------------------ -t_topic_rules(_Config) -> - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, +t_run_case(Config) -> + Case = ?config(test_case, Config), + ok = setup_source_data(Case), + ok = setup_authz_source(Case), + ok = emqx_authz_test_lib:run_checks(Case). - 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), - - 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 = [ +cases() -> + [ #{ - <<"x">> => #{ - <<"u">> => <<"username">>, - <<"c">> => [#{<<"c">> => <<"clientid">>}], - <<"y">> => 1 - }, - <<"permission">> => <<"allow">>, - <<"action">> => <<"publish">>, - <<"topics">> => [<<"t">>] - } - ], - - ok = setup_samples(Samples), - ok = setup_config( - #{ - <<"filter">> => #{ - <<"x">> => #{ - <<"u">> => <<"${username}">>, - <<"c">> => [#{<<"c">> => <<"${clientid}">>}], - <<"y">> => 1 + name => base_publish, + records => [ + #{ + <<"username">> => <<"username">>, + <<"action">> => <<"publish">>, + <<"topic">> => <<"a">>, + <<"permission">> => <<"allow">> + }, + #{ + <<"username">> => <<"username">>, + <<"action">> => <<"subscribe">>, + <<"topic">> => <<"b">>, + <<"permission">> => <<"allow">> + }, + #{ + <<"username">> => <<"username">>, + <<"action">> => <<"all">>, + <<"topics">> => [<<"c">>, <<"d">>], + <<"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 %%------------------------------------------------------------------------------ -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() -> {true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"acl">>, #{}), 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) -> emqx_authz_test_lib:setup_config( raw_mongo_authz_config(), diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 06449b3b4..f31a6ceab 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -27,10 +27,10 @@ -define(MYSQL_RESOURCE, <<"emqx_authz_mysql_SUITE">>). all() -> - emqx_common_test_helpers:all(?MODULE). + emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()). groups() -> - []. + emqx_authz_test_lib:table_groups(t_run_case, cases()). init_per_suite(Config) -> ok = stop_apps([emqx_resource]), @@ -41,13 +41,7 @@ init_per_suite(Config) -> fun set_special_configs/1 ), ok = start_apps([emqx_resource]), - {ok, _} = emqx_resource:create_local( - ?MYSQL_RESOURCE, - ?RESOURCE_GROUP, - emqx_mysql, - mysql_config(), - #{} - ), + ok = create_mysql_resource(), Config; false -> {skip, no_mysql} @@ -59,9 +53,18 @@ end_per_suite(_Config) -> ok = stop_apps([emqx_resource]), 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) -> ok = emqx_authz_test_lib:reset_authorizers(), Config. +end_per_testcase(_TestCase, _Config) -> + _ = emqx_authz:set_feature_available(rich_actions, true), + ok = drop_table(), + ok. set_special_configs(emqx_authz) -> ok = emqx_authz_test_lib:reset_authorizers(); @@ -72,189 +75,11 @@ set_special_configs(_) -> %% Testcases %%------------------------------------------------------------------------------ -t_topic_rules(_Config) -> - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - peerhost => {127, 0, 0, 1}, - 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_run_case(Config) -> + Case = ?config(test_case, Config), + ok = setup_source_data(Case), + ok = setup_authz_source(Case), + ok = emqx_authz_test_lib:run_checks(Case). t_create_invalid(_Config) -> BadConfig = maps:merge( @@ -265,45 +90,285 @@ t_create_invalid(_Config) -> [_] = emqx_authz:lookup(). -t_nonbinary_values(_Config) -> - ClientInfo = #{ - clientid => clientid, - username => "username", - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, +%%------------------------------------------------------------------------------ +%% Cases +%%------------------------------------------------------------------------------ - ok = init_table(), - ok = q( - << - "INSERT INTO acl(clientid, username, topic, permission, action)" - "VALUES(?, ?, ?, ?, ?)" - >>, - [<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>] - ), - - ok = setup_config( +cases() -> + [ #{ - <<"query">> => << - "SELECT permission, action, topic " - "FROM acl WHERE clientid = ${clientid} AND username = ${username}" - >> - } - ), + name => base_publish, + 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')", + "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( - ClientInfo, - [ - {allow, subscribe, <<"a">>}, - {deny, subscribe, <<"b">>} - ] - ). + "INSERT INTO acl(clientid, cn, dn, peerhost, topic, permission, action)" + " VALUES('clientid', 'cn', 'dn', '127.0.0.1', 'a', 'allow', 'publish')" + ], + query => + "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 %%------------------------------------------------------------------------------ +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() -> #{ <<"enable">> => <<"true">>, @@ -332,52 +397,9 @@ q(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() -> 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) -> emqx_authz_test_lib:setup_config( raw_mysql_authz_config(), @@ -400,3 +422,13 @@ start_apps(Apps) -> stop_apps(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. diff --git a/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl index 0ef21360c..0c446ee99 100644 --- a/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl @@ -27,10 +27,10 @@ -define(PGSQL_RESOURCE, <<"emqx_authz_pgsql_SUITE">>). all() -> - emqx_common_test_helpers:all(?MODULE). + emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()). groups() -> - []. + emqx_authz_test_lib:table_groups(t_run_case, cases()). init_per_suite(Config) -> ok = stop_apps([emqx_resource]), @@ -41,13 +41,7 @@ init_per_suite(Config) -> fun set_special_configs/1 ), ok = start_apps([emqx_resource]), - {ok, _} = emqx_resource:create_local( - ?PGSQL_RESOURCE, - ?RESOURCE_GROUP, - emqx_connector_pgsql, - pgsql_config(), - #{} - ), + {ok, _} = create_pgsql_resource(), Config; false -> {skip, no_pgsql} @@ -59,9 +53,18 @@ end_per_suite(_Config) -> ok = stop_apps([emqx_resource]), 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) -> ok = emqx_authz_test_lib:reset_authorizers(), Config. +end_per_testcase(_TestCase, _Config) -> + _ = emqx_authz:set_feature_available(rich_actions, true), + ok = drop_table(), + ok. set_special_configs(emqx_authz) -> ok = emqx_authz_test_lib:reset_authorizers(); @@ -72,194 +75,11 @@ set_special_configs(_) -> %% Testcases %%------------------------------------------------------------------------------ -t_topic_rules(_Config) -> - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - peerhost => {127, 0, 0, 1}, - 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_run_case(Config) -> + Case = ?config(test_case, Config), + ok = setup_source_data(Case), + ok = setup_authz_source(Case), + ok = emqx_authz_test_lib:run_checks(Case). t_create_invalid(_Config) -> BadConfig = maps:merge( @@ -270,45 +90,291 @@ t_create_invalid(_Config) -> [_] = emqx_authz:lookup(). -t_nonbinary_values(_Config) -> - ClientInfo = #{ - clientid => clientid, - username => "username", - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, +%%------------------------------------------------------------------------------ +%% Cases +%%------------------------------------------------------------------------------ - ok = init_table(), - ok = insert( - << - "INSERT INTO acl(clientid, username, topic, permission, action)" - "VALUES($1, $2, $3, $4, $5)" - >>, - [<<"clientid">>, <<"username">>, <<"a">>, <<"allow">>, <<"subscribe">>] - ), - - ok = setup_config( +cases() -> + [ #{ - <<"query">> => << - "SELECT permission, action, topic " - "FROM acl WHERE clientid = ${clientid} AND username = ${username}" - >> - } - ), + name => base_publish, + 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')", + "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( - ClientInfo, - [ - {allow, subscribe, <<"a">>}, - {deny, subscribe, <<"b">>} - ] - ). + "INSERT INTO acl(clientid, cn, dn, peerhost, topic, permission, action)" + " VALUES('clientid', 'cn', 'dn', '127.0.0.1', 'a', 'allow', 'publish')" + ], + query => + "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 %%------------------------------------------------------------------------------ +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() -> #{ <<"enable">> => <<"true">>, @@ -331,61 +397,10 @@ q(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() -> {ok, _, _} = q("DROP TABLE IF EXISTS acl"), 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) -> emqx_authz_test_lib:setup_config( raw_pgsql_authz_config(), @@ -403,6 +418,15 @@ pgsql_config() -> ssl => #{enable => false} }. +create_pgsql_resource() -> + emqx_resource:create_local( + ?PGSQL_RESOURCE, + ?RESOURCE_GROUP, + emqx_connector_pgsql, + pgsql_config(), + #{} + ). + start_apps(Apps) -> lists:foreach(fun application:ensure_all_started/1, Apps). diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 29a352970..28110a7a5 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -28,10 +28,10 @@ -define(REDIS_RESOURCE, <<"emqx_authz_redis_SUITE">>). all() -> - emqx_common_test_helpers:all(?MODULE). + emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()). groups() -> - []. + emqx_authz_test_lib:table_groups(t_run_case, cases()). init_per_suite(Config) -> ok = stop_apps([emqx_resource]), @@ -42,13 +42,7 @@ init_per_suite(Config) -> fun set_special_configs/1 ), ok = start_apps([emqx_resource]), - {ok, _} = emqx_resource:create_local( - ?REDIS_RESOURCE, - ?RESOURCE_GROUP, - emqx_redis, - redis_config(), - #{} - ), + ok = create_redis_resource(), Config; false -> {skip, no_redis} @@ -60,9 +54,18 @@ end_per_suite(_Config) -> ok = stop_apps([emqx_resource]), 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) -> ok = emqx_authz_test_lib:reset_authorizers(), Config. +end_per_testcase(_TestCase, _Config) -> + _ = emqx_authz:set_feature_available(rich_actions, true), + _ = cleanup_redis(), + ok. set_special_configs(emqx_authz) -> ok = emqx_authz_test_lib:reset_authorizers(); @@ -73,93 +76,11 @@ set_special_configs(_) -> %% Tests %%------------------------------------------------------------------------------ -t_topic_rules(_Config) -> - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - peerhost => {127, 0, 0, 1}, - 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">>} - ] - ). +t_run_case(Config) -> + Case = ?config(test_case, Config), + ok = setup_source_data(Case), + ok = setup_authz_source(Case), + ok = emqx_authz_test_lib:run_checks(Case). %% should still succeed to create even if the config will not work, %% because it's not a part of the schema check @@ -181,7 +102,7 @@ t_create_with_config_values_wont_work(_Config) -> InvalidConfigs ). -%% creating without a require field should return error +%% creating without a required field should return error t_create_invalid_config(_Config) -> AuthzConfig = raw_redis_authz_config(), C = maps:without([<<"server">>], AuthzConfig), @@ -196,54 +117,211 @@ t_create_invalid_config(_Config) -> t_redis_error(_Config) -> ok = setup_config(#{<<"cmd">> => <<"INVALID COMMAND">>}), - ClientInfo = #{ - clientid => <<"clientid">>, - username => <<"username">>, - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, + ClientInfo = emqx_authz_test_lib:base_client_info(), - 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 %%------------------------------------------------------------------------------ -setup_sample(AuthzData) -> - {ok, _} = q(["FLUSHDB"]), - ok = lists:foreach( - fun({Key, Values}) -> - lists:foreach( - fun({TopicFilter, Action}) -> - q(["HSET", Key, TopicFilter, Action]) - end, - maps:to_list(Values) - ) +setup_source_data(#{setup := Queries}) -> + lists:foreach( + fun(Query) -> + _ = q(Query) end, - maps:to_list(AuthzData) + Queries ). -setup_client_samples(ClientInfo, Samples) -> - #{username := Username} = ClientInfo, - Key = <<"mqtt_user:", Username/binary>>, - lists:foreach( - 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_authz_source(#{cmd := Cmd}) -> + setup_config( + #{ + <<"cmd">> => Cmd + } + ). setup_config(SpecialParams) -> Config = maps:merge(raw_redis_authz_config(), SpecialParams), @@ -261,6 +339,9 @@ raw_redis_authz_config() -> <<"server">> => <> }. +cleanup_redis() -> + q([<<"FLUSHALL">>]). + q(Command) -> emqx_resource:simple_sync_query( ?REDIS_RESOURCE, @@ -283,3 +364,13 @@ start_apps(Apps) -> stop_apps(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. diff --git a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl index fbfb84785..c73fe96ea 100644 --- a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl @@ -18,24 +18,17 @@ -compile(nowarn_export_all). -compile(export_all). --include("emqx_authz.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). --define(SOURCE1, {deny, all}). --define(SOURCE2, {allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}). --define(SOURCE3, {allow, {ipaddrs, ["127.0.0.1", "192.168.1.0/24"]}, subscribe, [?PH_S_CLIENTID]}). --define(SOURCE4, {allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]}). --define(SOURCE5, - {allow, - {'or', [ - {username, {re, "^test"}}, - {clientid, {re, "test?"}} - ]}, - publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]} -). --define(SOURCE6, {allow, {username, "test"}, publish, ["t/foo${username}boo"]}). +-define(CLIENT_INFO_BASE, #{ + clientid => <<"test">>, + username => <<"test">>, + peerhost => {127, 0, 0, 1}, + zone => default, + listener => {tcp, default} +}). all() -> emqx_common_test_helpers:all(?MODULE). @@ -59,6 +52,12 @@ end_per_suite(_Config) -> emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]), 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) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), @@ -68,11 +67,11 @@ set_special_configs(_App) -> ok. t_compile(_) -> - ?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile(?SOURCE1)), + ?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile({deny, all})), ?assertEqual( {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( @@ -82,14 +81,18 @@ t_compile(_) -> {{192, 168, 1, 0}, {192, 168, 1, 255}, 24} ]}, 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, [ [<<"topic">>, <<"test">>] ]}, - emqx_authz_rule:compile(?SOURCE4) + emqx_authz_rule:compile( + {allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]} + ) ), ?assertMatch( @@ -101,240 +104,643 @@ t_compile(_) -> publish, [ {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( {allow, {username, {eq, <<"test">>}}, publish, [ {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. +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(_) -> - ClientInfo1 = #{ - clientid => <<"test">>, - username => <<"test">>, - peerhost => {127, 0, 0, 1}, - zone => default, - listener => {tcp, default} - }, - 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( + {matched, deny}, + emqx_authz_rule:match( + client_info(), + #{action_type => subscribe, qos => 0}, + <<"#">>, + emqx_authz_rule:compile({deny, all}) + ) + ), ?assertEqual( {matched, deny}, emqx_authz_rule:match( - ClientInfo1, - subscribe, - <<"#">>, - emqx_authz_rule:compile(?SOURCE1) - ) - ), - ?assertEqual( - {matched, deny}, - emqx_authz_rule:match( - ClientInfo2, - subscribe, + client_info(#{peerhost => {192, 168, 1, 10}}), + #{action_type => subscribe, qos => 0}, <<"+">>, - emqx_authz_rule:compile(?SOURCE1) + emqx_authz_rule:compile({deny, all}) ) ), + ?assertEqual( {matched, deny}, emqx_authz_rule:match( - ClientInfo3, - subscribe, + client_info(#{username => <<"fake">>}), + #{action_type => subscribe, qos => 0}, <<"topic/test">>, - emqx_authz_rule:compile(?SOURCE1) + emqx_authz_rule:compile({deny, all}) ) ), ?assertEqual( {matched, allow}, emqx_authz_rule:match( - ClientInfo1, - subscribe, + client_info(), + #{action_type => subscribe, qos => 0}, <<"#">>, - emqx_authz_rule:compile(?SOURCE2) + emqx_authz_rule:compile({allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}) ) ), + ?assertEqual( nomatch, emqx_authz_rule:match( - ClientInfo1, - subscribe, + client_info(), + #{action_type => subscribe, qos => 0}, <<"topic/test">>, - emqx_authz_rule:compile(?SOURCE2) + emqx_authz_rule:compile({allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}) ) ), + ?assertEqual( nomatch, emqx_authz_rule:match( - ClientInfo2, - subscribe, + client_info(#{peerhost => {192, 168, 1, 10}}), + #{action_type => subscribe, qos => 0}, <<"#">>, - emqx_authz_rule:compile(?SOURCE2) + emqx_authz_rule:compile({allow, {ipaddr, "127.0.0.1"}, all, [{eq, "#"}, {eq, "+"}]}) ) ), ?assertEqual( {matched, allow}, emqx_authz_rule:match( - ClientInfo1, - subscribe, + client_info(), + #{action_type => subscribe, qos => 0}, <<"test">>, - emqx_authz_rule:compile(?SOURCE3) - ) - ), - ?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) + 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( - ClientInfo1, - publish, - <<"topic/test">>, - emqx_authz_rule:compile(?SOURCE4) - ) - ), - ?assertEqual( - {matched, allow}, - emqx_authz_rule:match( - ClientInfo2, - publish, - <<"topic/test">>, - emqx_authz_rule:compile(?SOURCE4) + client_info(#{peerhost => {192, 168, 1, 10}}), + #{action_type => subscribe, qos => 0}, + <<"test">>, + 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( - ClientInfo3, - publish, + client_info(#{peerhost => {192, 168, 1, 10}}), + #{action_type => subscribe, qos => 0}, <<"topic/test">>, - emqx_authz_rule:compile(?SOURCE4) - ) - ), - ?assertEqual( - nomatch, - emqx_authz_rule:match( - ClientInfo4, - publish, - <<"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( {matched, allow}, emqx_authz_rule:match( - ClientInfo1, - publish, - <<"test">>, - emqx_authz_rule:compile(?SOURCE5) + client_info(), + #{action_type => publish, qos => 0, retain => false}, + <<"topic/test">>, + emqx_authz_rule:compile( + {allow, {'and', [{client, "test"}, {user, "test"}]}, publish, ["topic/test"]} + ) ) ), + ?assertEqual( {matched, allow}, emqx_authz_rule:match( - ClientInfo2, - publish, - <<"test">>, - emqx_authz_rule:compile(?SOURCE5) + client_info(#{peerhost => {192, 168, 1, 10}}), + #{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(#{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( {matched, allow}, emqx_authz_rule:match( - ClientInfo3, - publish, + client_info(), + #{action_type => publish, qos => 0, retain => false}, <<"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( {matched, allow}, emqx_authz_rule:match( - ClientInfo3, - publish, + client_info(#{peerhost => {192, 168, 1, 10}}), + #{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">>, - 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( {matched, allow}, emqx_authz_rule:match( - ClientInfo4, - publish, + client_info(#{clientid => <<"fake">>}), + #{action_type => publish, qos => 0, retain => false}, <<"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( {matched, allow}, emqx_authz_rule:match( - ClientInfo4, - publish, + client_info(#{clientid => <<"fake">>}), + #{action_type => publish, qos => 0, retain => false}, <<"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( nomatch, emqx_authz_rule:match( - ClientInfo1, - publish, + client_info(), + #{action_type => publish, qos => 0, retain => false}, <<"t/foo${username}boo">>, - emqx_authz_rule:compile(?SOURCE6) + emqx_authz_rule:compile({allow, {username, "test"}, publish, ["t/foo${username}boo"]}) ) ), ?assertEqual( {matched, allow}, emqx_authz_rule:match( - ClientInfo4, - publish, + client_info(#{clientid => <<"fake">>}), + #{action_type => publish, qos => 0, retain => false}, <<"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. + +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). diff --git a/apps/emqx_authz/test/emqx_authz_rule_raw_SUITE.erl b/apps/emqx_authz/test/emqx_authz_rule_raw_SUITE.erl new file mode 100644 index 000000000..8b097c3fd --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_rule_raw_SUITE.erl @@ -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}. diff --git a/apps/emqx_authz/test/emqx_authz_test_lib.erl b/apps/emqx_authz/test/emqx_authz_test_lib.erl index 03d687851..ea62c5b71 100644 --- a/apps/emqx_authz/test/emqx_authz_test_lib.erl +++ b/apps/emqx_authz/test/emqx_authz_test_lib.erl @@ -22,8 +22,6 @@ -compile(nowarn_export_all). -compile(export_all). --define(DEFAULT_CHECK_AVAIL_TIMEOUT, 1000). - reset_authorizers() -> reset_authorizers(deny, false, []). @@ -53,216 +51,68 @@ setup_config(BaseConfig, SpecialParams) -> {error, Reason} -> {error, Reason} 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( - fun({Expected, Action, Topic}) -> - ct:pal( - "client_info: ~p, action: ~p, topic: ~p, expected: ~p", - [ClientInfo, Action, Topic, Expected] - ), - ?assertEqual( - Expected, - emqx_access_control:authorize( - ClientInfo, - Action, - Topic - ) - ) + fun(Feature) -> + Enable = lists:member(Feature, Features), + emqx_authz:set_feature_available(Feature, Enable) end, - Samples + ?AUTHZ_FEATURES ). -test_no_topic_rules(ClientInfo, SetupSamples) -> - %% No rules - - ok = reset_authorizers(deny, false), - ok = SetupSamples(ClientInfo, []), - - ok = test_samples( - ClientInfo, - [ - {deny, subscribe, <<"#">>}, - {deny, subscribe, <<"subs">>}, - {deny, publish, <<"pub">>} - ] +run_checks(#{checks := Checks} = Case) -> + _ = setup_default_permission(Case), + _ = enable_features(Case), + ClientInfoOverrides = maps:get(client_info, Case, #{}), + ClientInfo = client_info(ClientInfoOverrides), + lists:foreach( + fun(Check) -> + run_check(ClientInfo, Check) + end, + Checks ). -test_allow_topic_rules(ClientInfo, SetupSamples) -> - Samples = [ - #{ - topics => [ - <<"eq testpub1/${username}">>, - <<"testpub2/${clientid}">>, - <<"testpub3/#">> - ], - 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">>} - ] +run_check(ClientInfo, {ExpectedPermission, Action, Topic}) -> + ?assertEqual( + ExpectedPermission, + emqx_access_control:authorize( + ClientInfo, + Action, + Topic + ) ). diff --git a/apps/emqx_exhook/src/emqx_exhook.app.src b/apps/emqx_exhook/src/emqx_exhook.app.src index 92a70cf37..8a57249e9 100644 --- a/apps/emqx_exhook/src/emqx_exhook.app.src +++ b/apps/emqx_exhook/src/emqx_exhook.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_exhook, [ {description, "EMQX Extension for Hook"}, - {vsn, "5.0.13"}, + {vsn, "5.0.14"}, {modules, []}, {registered, []}, {mod, {emqx_exhook_app, []}}, diff --git a/apps/emqx_exhook/src/emqx_exhook_handler.erl b/apps/emqx_exhook/src/emqx_exhook_handler.erl index 8720f65ae..b4358969d 100644 --- a/apps/emqx_exhook/src/emqx_exhook_handler.erl +++ b/apps/emqx_exhook/src/emqx_exhook_handler.erl @@ -16,9 +16,9 @@ -module(emqx_exhook_handler). --include("emqx_exhook.hrl"). -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("emqx/include/emqx_access_control.hrl"). -export([ on_client_connect/2, @@ -132,12 +132,13 @@ on_client_authenticate(ClientInfo, AuthResult) -> {ok, AuthResult} end. -on_client_authorize(ClientInfo, PubSub, Topic, Result) -> +on_client_authorize(ClientInfo, Action, Topic, Result) -> Bool = maps:get(result, Result, deny) == allow, + %% TODO: Support full action in major release Type = - case PubSub of - publish -> 'PUBLISH'; - subscribe -> 'SUBSCRIBE' + case Action of + ?authz_action(publish) -> 'PUBLISH'; + ?authz_action(subscribe) -> 'SUBSCRIBE' end, Req = #{ clientinfo => clientinfo(ClientInfo), diff --git a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl index bd756620d..3da73c11a 100644 --- a/apps/emqx_exhook/test/emqx_exhook_SUITE.erl +++ b/apps/emqx_exhook/test/emqx_exhook_SUITE.erl @@ -19,7 +19,7 @@ -compile(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("common_test/include/ct.hrl"). @@ -126,7 +126,7 @@ t_access_failed_if_no_server_running(Config) -> allow, emqx_access_control:authorize( ClientInfo#{username => <<"gooduser">>}, - publish, + ?AUTHZ_PUBLISH, <<"acl/1">> ) ), @@ -135,7 +135,7 @@ t_access_failed_if_no_server_running(Config) -> deny, emqx_access_control:authorize( ClientInfo#{username => <<"baduser">>}, - publish, + ?AUTHZ_PUBLISH, <<"acl/2">> ) ), @@ -148,7 +148,7 @@ t_access_failed_if_no_server_running(Config) -> ?assertMatch( {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 }) ), diff --git a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl index 075cc736c..34d7a4342 100644 --- a/apps/emqx_exhook/test/props/prop_exhook_hooks.erl +++ b/apps/emqx_exhook/test/props/prop_exhook_hooks.erl @@ -18,6 +18,7 @@ -include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include_lib("emqx/include/emqx_access_control.hrl"). -import( emqx_proper_types, @@ -29,7 +30,8 @@ connack_return_code/0, topictab/0, topic/0, - subopts/0 + subopts/0, + pubsub/0 ] ). @@ -138,7 +140,7 @@ prop_client_authorize() -> {ClientInfo0, PubSub, Topic, Result, Meta}, { clientinfo(), - oneof([publish, subscribe]), + pubsub(), topic(), oneof([MkResult(allow), MkResult(deny)]), request_meta() @@ -554,8 +556,8 @@ authresult_to_bool(AuthResult) -> aclresult_to_bool(#{result := Result}) -> Result == allow. -pubsub_to_enum(publish) -> 'PUBLISH'; -pubsub_to_enum(subscribe) -> 'SUBSCRIBE'. +pubsub_to_enum(?authz_action(publish)) -> 'PUBLISH'; +pubsub_to_enum(?authz_action(subscribe)) -> 'SUBSCRIBE'. from_conninfo(ConnInfo) -> #{ diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index 32e5fcf96..11ad55d3e 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -162,8 +162,8 @@ connection_closed(_Ctx = #{gwname := GwName}, ClientId) -> emqx_types:topic() ) -> allow | deny. -authorize(_Ctx, ClientInfo, PubSub, Topic) -> - emqx_access_control:authorize(ClientInfo, PubSub, Topic). +authorize(_Ctx, ClientInfo, Action, Topic) -> + emqx_access_control:authorize(ClientInfo, Action, Topic). metrics_inc(_Ctx = #{gwname := GwName}, Name) -> emqx_gateway_metrics:inc(GwName, Name). diff --git a/apps/emqx_gateway_coap/src/emqx_coap_channel.erl b/apps/emqx_gateway_coap/src/emqx_coap_channel.erl index d95dc5bd2..0c0b7310d 100644 --- a/apps/emqx_gateway_coap/src/emqx_coap_channel.erl +++ b/apps/emqx_gateway_coap/src/emqx_coap_channel.erl @@ -47,9 +47,6 @@ -include("emqx_coap.hrl"). -include_lib("emqx/include/logger.hrl"). --include_lib("emqx/include/emqx_authentication.hrl"). - --define(AUTHN, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM). -record(channel, { %% Context @@ -166,8 +163,8 @@ init( conn_state = idle }. -validator(Type, Topic, Ctx, ClientInfo) -> - emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). +validator(Action, Topic, Ctx, ClientInfo) -> + emqx_gateway_ctx:authorize(Ctx, ClientInfo, Action, Topic). -spec send_request(pid(), coap_message()) -> any(). send_request(Channel, Request) -> diff --git a/apps/emqx_gateway_coap/src/emqx_coap_pubsub_handler.erl b/apps/emqx_gateway_coap/src/emqx_coap_pubsub_handler.erl index da1f5e0ef..3070ea891 100644 --- a/apps/emqx_gateway_coap/src/emqx_coap_pubsub_handler.erl +++ b/apps/emqx_gateway_coap/src/emqx_coap_pubsub_handler.erl @@ -18,6 +18,7 @@ -module(emqx_coap_pubsub_handler). -include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("emqx/include/emqx_access_control.hrl"). -include("emqx_coap.hrl"). -export([handle_request/4]). @@ -50,14 +51,16 @@ handle_method(get, Topic, Msg, Ctx, CInfo) -> reply({error, bad_request}, <<"invalid observe value">>, Msg) end; 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 -> #{clientid := ClientId} = CInfo, MountTopic = mount(CInfo, Topic), - QOS = get_publish_qos(Msg), %% TODO: Append message metadata into headers - MQTTMsg = emqx_message:make(ClientId, QOS, MountTopic, Payload), - MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg), + MQTTMsg = emqx_message:make(ClientId, Qos, MountTopic, Payload), + MQTTMsg2 = apply_publish_opts(PublishOpts, MQTTMsg), _ = emqx_broker:publish(MQTTMsg2), reply({ok, changed}, Msg); _ -> @@ -104,48 +107,70 @@ type_to_qos(coap, #coap_message{type = Type}) -> ?QOS_1 end. -get_publish_qos(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) -> +get_publish_opts(Msg) -> case emqx_coap_message:get_option(uri_query, Msg) of undefined -> - MQTTMsg; + #{}; Qs -> maps:fold( fun (<<"retain">>, V, Acc) -> Val = V =:= <<"true">>, - emqx_message:set_flag(retain, Val, Acc); + Acc#{retain => Val}; (<<"expiry">>, V, Acc) -> Val = erlang:binary_to_integer(V), - Props = emqx_message:get_header(properties, Acc), - emqx_message:set_header( - properties, - Props#{'Message-Expiry-Interval' => Val}, - Acc - ); + Acc#{expiry_interval => Val}; + (<<"qos">>, V, Acc) -> + Val = erlang:binary_to_integer(V), + Acc#{qos => Val}; (_, _, Acc) -> Acc end, - MQTTMsg, + #{}, Qs ) 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, _, _, _) -> reply({error, bad_request}, <<"observe without token">>, Msg); 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 -> #{clientid := ClientId} = CInfo, - SubOpts = get_sub_opts(Msg), + MountTopic = mount(CInfo, Topic), emqx_broker:subscribe(MountTopic, ClientId, SubOpts), run_hooks(Ctx, 'session.subscribed', [CInfo, MountTopic, SubOpts]), diff --git a/apps/emqx_gateway_exproto/src/emqx_exproto_channel.erl b/apps/emqx_gateway_exproto/src/emqx_exproto_channel.erl index 5de597920..80d3282c5 100644 --- a/apps/emqx_gateway_exproto/src/emqx_exproto_channel.erl +++ b/apps/emqx_gateway_exproto/src/emqx_exproto_channel.erl @@ -19,6 +19,7 @@ -include("emqx_exproto.hrl"). -include_lib("emqx/include/emqx.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/logger.hrl"). @@ -428,7 +429,8 @@ handle_call( 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 -> {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 -> {reply, {error, ?RESP_PERMISSION_DENY, <<"Authorization deny">>}, Channel}; _ -> diff --git a/apps/emqx_gateway_lwm2m/src/emqx_lwm2m_channel.erl b/apps/emqx_gateway_lwm2m/src/emqx_lwm2m_channel.erl index bbd2d4377..e187b3fb7 100644 --- a/apps/emqx_gateway_lwm2m/src/emqx_lwm2m_channel.erl +++ b/apps/emqx_gateway_lwm2m/src/emqx_lwm2m_channel.erl @@ -17,7 +17,9 @@ -module(emqx_lwm2m_channel). -include("emqx_lwm2m.hrl"). +-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("emqx/include/emqx_access_control.hrl"). -include_lib("emqx_gateway_coap/include/emqx_coap.hrl"). %% API @@ -644,7 +646,8 @@ with_context(Ctx, ClientInfo) -> end. 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 -> _ = emqx_broker:publish(Msg), ok; @@ -660,7 +663,8 @@ with_context(subscribe, [Topic, Opts], Ctx, ClientInfo) -> clientid := ClientId, endpoint_name := EndpointName } = 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 -> run_hooks(Ctx, 'session.subscribed', [ClientInfo, Topic, Opts]), ?SLOG(debug, #{ @@ -681,6 +685,14 @@ with_context(subscribe, [Topic, Opts], Ctx, ClientInfo) -> with_context(metrics, Name, Ctx, _ClientInfo) -> 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 %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway_mqttsn/src/emqx_gateway_mqttsn.app.src b/apps/emqx_gateway_mqttsn/src/emqx_gateway_mqttsn.app.src index b43201e1a..5e79d4d49 100644 --- a/apps/emqx_gateway_mqttsn/src/emqx_gateway_mqttsn.app.src +++ b/apps/emqx_gateway_mqttsn/src/emqx_gateway_mqttsn.app.src @@ -1,6 +1,6 @@ {application, emqx_gateway_mqttsn, [ {description, "MQTT-SN Gateway"}, - {vsn, "0.1.2"}, + {vsn, "0.1.3"}, {registered, []}, {applications, [kernel, stdlib, emqx, emqx_gateway]}, {env, []}, diff --git a/apps/emqx_gateway_mqttsn/src/emqx_mqttsn_channel.erl b/apps/emqx_gateway_mqttsn/src/emqx_mqttsn_channel.erl index 720c288d3..2443b149a 100644 --- a/apps/emqx_gateway_mqttsn/src/emqx_mqttsn_channel.erl +++ b/apps/emqx_gateway_mqttsn/src/emqx_mqttsn_channel.erl @@ -22,6 +22,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/types.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("emqx/include/emqx_access_control.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). @@ -1099,10 +1100,11 @@ convert_topic_id_to_name( end. check_pub_authz( - {TopicName, _Flags, _Data}, + {TopicName, #mqtt_sn_flags{qos = QoS, retain = Retain}, _Data}, #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; deny -> {error, ?SN_RC2_NOT_AUTHORIZE} end. @@ -1251,10 +1253,11 @@ preproc_subs_type( {error, ?SN_RC_NOT_SUPPORTED}. check_subscribe_authz( - {_TopicId, TopicName, _QoS}, + {_TopicId, TopicName, QoS}, 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 -> {ok, Channel}; _ -> diff --git a/apps/emqx_gateway_stomp/src/emqx_stomp_channel.erl b/apps/emqx_gateway_stomp/src/emqx_stomp_channel.erl index 3577c0a60..3ae928ba3 100644 --- a/apps/emqx_gateway_stomp/src/emqx_stomp_channel.erl +++ b/apps/emqx_gateway_stomp/src/emqx_stomp_channel.erl @@ -20,6 +20,7 @@ -include("emqx_stomp.hrl"). -include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_access_control.hrl"). -include_lib("emqx/include/logger.hrl"). -import(proplists, [get_value/2, get_value/3]). @@ -446,7 +447,10 @@ handle_in( } ) -> 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 -> ErrMsg = io_lib:format("Insufficient permissions for ~s", [Topic]), ErrorFrame = error_frame(receipt_id(Headers), ErrMsg), @@ -717,7 +721,9 @@ check_sub_acl( 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}; allow -> ok end. diff --git a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl index 6e3768431..47756cc4c 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl @@ -89,15 +89,15 @@ t_clients(_) -> AfterKickoutResponse2 = emqx_mgmt_api_test_util:request_api(get, Client2Path), ?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([ "clients", binary_to_list(ClientId1), "authorization", "cache" ]), - {ok, Client1AuthzCache} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath), - ?assertEqual("[]", Client1AuthzCache), + {ok, Client1AuthzCache0} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath), + ?assertEqual("[]", Client1AuthzCache0), %% post /clients/:clientid/subscribe 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), ?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(_) -> process_flag(trap_exit, true), diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 7f14f6d8b..3ff588f48 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -20,6 +20,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.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"). -export([ @@ -160,7 +161,10 @@ on_client_connack(ConnInfo, Reason, _, 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( 'client.check_authz_complete', fun() -> diff --git a/changes/ee/feat-11132.en.md b/changes/ee/feat-11132.en.md new file mode 100644 index 000000000..6ebc7efe2 --- /dev/null +++ b/changes/ee/feat-11132.en.md @@ -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. diff --git a/rel/i18n/emqx_authz_api_mnesia.hocon b/rel/i18n/emqx_authz_api_mnesia.hocon index d0021c6a5..0a3b4aba4 100644 --- a/rel/i18n/emqx_authz_api_mnesia.hocon +++ b/rel/i18n/emqx_authz_api_mnesia.hocon @@ -1,10 +1,20 @@ emqx_authz_api_mnesia { action.desc: -"""Authorized action (pub/sub/all)""" +"""Authorized action (publish/subscribe/all)""" action.label: """action""" +qos.desc: +"""QoS of authorized action""" +qos.label: +"""QoS""" + +retain.desc: +"""Retain flag of authorized action""" +retain.label: +"""retain""" + clientid.desc: """ClientID""" clientid.label: