feat(authz): add tests for authz extended actions
This commit is contained in:
parent
7de26a1776
commit
1ce6a225ae
8
Makefile
8
Makefile
|
@ -130,14 +130,14 @@ $(foreach app,$(APPS),$(eval $(call gen-app-prop-target,$(app))))
|
||||||
ct-suite: $(REBAR) merge-config clean-test-cluster-config
|
ct-suite: $(REBAR) merge-config clean-test-cluster-config
|
||||||
ifneq ($(TESTCASE),)
|
ifneq ($(TESTCASE),)
|
||||||
ifneq ($(GROUP),)
|
ifneq ($(GROUP),)
|
||||||
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE) --group $(GROUP)
|
$(REBAR) ct -c -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE) --group $(GROUP)
|
||||||
else
|
else
|
||||||
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE)
|
$(REBAR) ct -c -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE)
|
||||||
endif
|
endif
|
||||||
else ifneq ($(GROUP),)
|
else ifneq ($(GROUP),)
|
||||||
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --group $(GROUP)
|
$(REBAR) ct -c -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --group $(GROUP)
|
||||||
else
|
else
|
||||||
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE)
|
$(REBAR) ct -c -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: cover
|
.PHONY: cover
|
||||||
|
|
|
@ -77,14 +77,7 @@ authorize(
|
||||||
}
|
}
|
||||||
) ->
|
) ->
|
||||||
RenderedFilter = emqx_authz_utils:render_deep(FilterTemplate, Client),
|
RenderedFilter = emqx_authz_utils:render_deep(FilterTemplate, Client),
|
||||||
Result =
|
case emqx_resource:simple_sync_query(ResourceID, {find, Collection, RenderedFilter, #{}}) of
|
||||||
try
|
|
||||||
emqx_resource:simple_sync_query(ResourceID, {find, Collection, RenderedFilter, #{}})
|
|
||||||
catch
|
|
||||||
error:Error -> {error, Error}
|
|
||||||
end,
|
|
||||||
|
|
||||||
case Result of
|
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{
|
?SLOG(error, #{
|
||||||
msg => "query_mongo_error",
|
msg => "query_mongo_error",
|
||||||
|
@ -94,8 +87,6 @@ authorize(
|
||||||
resource_id => ResourceID
|
resource_id => ResourceID
|
||||||
}),
|
}),
|
||||||
nomatch;
|
nomatch;
|
||||||
{ok, []} ->
|
|
||||||
nomatch;
|
|
||||||
{ok, Rows} ->
|
{ok, Rows} ->
|
||||||
Rules = lists:flatmap(fun parse_rule/1, Rows),
|
Rules = lists:flatmap(fun parse_rule/1, Rows),
|
||||||
do_authorize(Client, Action, Topic, Rules)
|
do_authorize(Client, Action, Topic, Rules)
|
||||||
|
|
|
@ -86,8 +86,6 @@ authorize(
|
||||||
case
|
case
|
||||||
emqx_resource:simple_sync_query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams})
|
emqx_resource:simple_sync_query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams})
|
||||||
of
|
of
|
||||||
{ok, _ColumnNames, []} ->
|
|
||||||
nomatch;
|
|
||||||
{ok, ColumnNames, Rows} ->
|
{ok, ColumnNames, Rows} ->
|
||||||
do_authorize(Client, Action, Topic, ColumnNames, Rows);
|
do_authorize(Client, Action, Topic, ColumnNames, Rows);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
|
@ -93,8 +93,6 @@ authorize(
|
||||||
case
|
case
|
||||||
emqx_resource:simple_sync_query(ResourceID, {prepared_query, ResourceID, RenderedParams})
|
emqx_resource:simple_sync_query(ResourceID, {prepared_query, ResourceID, RenderedParams})
|
||||||
of
|
of
|
||||||
{ok, _Columns, []} ->
|
|
||||||
nomatch;
|
|
||||||
{ok, Columns, Rows} ->
|
{ok, Columns, Rows} ->
|
||||||
do_authorize(Client, Action, Topic, column_names(Columns), Rows);
|
do_authorize(Client, Action, Topic, column_names(Columns), Rows);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
|
|
@ -80,8 +80,6 @@ authorize(
|
||||||
Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
|
Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
|
||||||
Cmd = emqx_authz_utils:render_deep(CmdTemplate, Vars),
|
Cmd = emqx_authz_utils:render_deep(CmdTemplate, Vars),
|
||||||
case emqx_resource:simple_sync_query(ResourceID, {cmd, Cmd}) of
|
case emqx_resource:simple_sync_query(ResourceID, {cmd, Cmd}) of
|
||||||
{ok, []} ->
|
|
||||||
nomatch;
|
|
||||||
{ok, Rows} ->
|
{ok, Rows} ->
|
||||||
do_authorize(Client, Action, Topic, Rows);
|
do_authorize(Client, Action, Topic, Rows);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
@ -108,12 +106,13 @@ do_authorize(Client, Action, Topic, [TopicFilterRaw, RuleEncoded | Tail]) ->
|
||||||
{matched, Permission} -> {matched, Permission};
|
{matched, Permission} -> {matched, Permission};
|
||||||
nomatch -> do_authorize(Client, Action, Topic, Tail)
|
nomatch -> do_authorize(Client, Action, Topic, Tail)
|
||||||
catch
|
catch
|
||||||
error:Reason ->
|
error:Reason:Stack ->
|
||||||
?SLOG(error, #{
|
?SLOG(error, #{
|
||||||
msg => "match_rule_error",
|
msg => "match_rule_error",
|
||||||
reason => Reason,
|
reason => Reason,
|
||||||
rule_encoded => RuleEncoded,
|
rule_encoded => RuleEncoded,
|
||||||
topic_filter_raw => TopicFilterRaw
|
topic_filter_raw => TopicFilterRaw,
|
||||||
|
stacktrace => Stack
|
||||||
}),
|
}),
|
||||||
do_authorize(Client, Action, Topic, Tail)
|
do_authorize(Client, Action, Topic, Tail)
|
||||||
end.
|
end.
|
||||||
|
@ -148,6 +147,8 @@ parse_rule(Bin) when is_binary(Bin) ->
|
||||||
case emqx_utils_json:safe_decode(Bin, [return_maps]) of
|
case emqx_utils_json:safe_decode(Bin, [return_maps]) of
|
||||||
{ok, Map} when is_map(Map) ->
|
{ok, Map} when is_map(Map) ->
|
||||||
maps:with([<<"qos">>, <<"action">>, <<"retain">>], Map);
|
maps:with([<<"qos">>, <<"action">>, <<"retain">>], Map);
|
||||||
|
{ok, _} ->
|
||||||
|
error({invalid_topic_rule, Bin, notamap});
|
||||||
{error, Error} ->
|
{error, Error} ->
|
||||||
error({invalid_topic_rule, Bin, Error})
|
error({invalid_topic_rule, Bin, Error})
|
||||||
end.
|
end.
|
||||||
|
|
|
@ -38,21 +38,19 @@ all() ->
|
||||||
groups() ->
|
groups() ->
|
||||||
[].
|
[].
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
|
||||||
Config.
|
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
|
||||||
ok = emqx_authz_test_lib:restore_authorizers().
|
|
||||||
|
|
||||||
init_per_testcase(TestCase, Config) ->
|
init_per_testcase(TestCase, Config) ->
|
||||||
Apps = emqx_cth_suite:start(
|
Apps = emqx_cth_suite:start(
|
||||||
[{emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"}, emqx_authz],
|
[
|
||||||
|
{emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"},
|
||||||
|
emqx_authz
|
||||||
|
],
|
||||||
#{work_dir => filename:join(?config(priv_dir, Config), TestCase)}
|
#{work_dir => filename:join(?config(priv_dir, Config), TestCase)}
|
||||||
),
|
),
|
||||||
[{tc_apps, Apps} | Config].
|
[{tc_apps, Apps} | Config].
|
||||||
|
|
||||||
end_per_testcase(_TestCase, Config) ->
|
end_per_testcase(_TestCase, Config) ->
|
||||||
emqx_cth_suite:stop(?config(tc_apps, Config)).
|
emqx_cth_suite:stop(?config(tc_apps, Config)),
|
||||||
|
_ = emqx_authz:set_feature_available(rich_actions, true).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Testcases
|
%% Testcases
|
||||||
|
|
|
@ -333,19 +333,41 @@ cases() ->
|
||||||
},
|
},
|
||||||
#{
|
#{
|
||||||
name => invalid_query,
|
name => invalid_query,
|
||||||
setup => [],
|
setup => [
|
||||||
query => "SELECT permission, action, topic FROM acl WHER",
|
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
|
||||||
|
"permission VARCHAR(255), action VARCHAR(255))"
|
||||||
|
],
|
||||||
|
query => "SELECT permission, action, topic FRO",
|
||||||
checks => [
|
checks => [
|
||||||
{deny, ?AUTHZ_PUBLISH, <<"a">>}
|
{deny, ?AUTHZ_PUBLISH, <<"a">>}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
#{
|
#{
|
||||||
name => pgsql_error,
|
name => runtime_error,
|
||||||
setup => [],
|
setup => [
|
||||||
|
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
|
||||||
|
"permission VARCHAR(255), action VARCHAR(255))"
|
||||||
|
],
|
||||||
query =>
|
query =>
|
||||||
"SELECT permission, action, topic FROM table_not_exists WHERE username = ${username}",
|
"SELECT permission, action, topic FROM acl WHERE username = ${username}",
|
||||||
checks => [
|
checks => [
|
||||||
{deny, ?AUTHZ_PUBLISH, <<"t">>}
|
fun() ->
|
||||||
|
_ = q("DROP TABLE IF EXISTS acl"),
|
||||||
|
{deny, ?AUTHZ_PUBLISH, <<"t">>}
|
||||||
|
end
|
||||||
|
]
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
name => invalid_rule,
|
||||||
|
setup => [
|
||||||
|
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
|
||||||
|
"permission VARCHAR(255), action VARCHAR(255))",
|
||||||
|
%% 'permit' is invalid value for action
|
||||||
|
"INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'permit', 'publish')"
|
||||||
|
],
|
||||||
|
query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
|
||||||
|
checks => [
|
||||||
|
{deny, ?AUTHZ_PUBLISH, <<"a">>}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
].
|
].
|
||||||
|
|
|
@ -352,6 +352,19 @@ cases() ->
|
||||||
checks => [
|
checks => [
|
||||||
{deny, ?AUTHZ_PUBLISH, <<"t">>}
|
{deny, ?AUTHZ_PUBLISH, <<"t">>}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
name => invalid_rule,
|
||||||
|
setup => [
|
||||||
|
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
|
||||||
|
"permission VARCHAR(255), action VARCHAR(255))",
|
||||||
|
%% 'permit' is invalid value for action
|
||||||
|
"INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'permit', 'publish')"
|
||||||
|
],
|
||||||
|
query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
|
||||||
|
checks => [
|
||||||
|
{deny, ?AUTHZ_PUBLISH, <<"a">>}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
%% TODO: add case for unknown variables after fixing EMQX-10400
|
%% TODO: add case for unknown variables after fixing EMQX-10400
|
||||||
].
|
].
|
||||||
|
|
|
@ -181,6 +181,14 @@ ok_cases() ->
|
||||||
expected_rule_with_qos_retain([0, 1, 2], all),
|
expected_rule_with_qos_retain([0, 1, 2], all),
|
||||||
rule_with_raw_qos_retain(#{})
|
rule_with_raw_qos_retain(#{})
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
expected_rule_with_qos_retain([0, 1, 2], true),
|
||||||
|
rule_with_raw_qos_retain(#{<<"retain">> => <<"1">>})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expected_rule_with_qos_retain([0, 1, 2], false),
|
||||||
|
rule_with_raw_qos_retain(#{<<"retain">> => <<"0">>})
|
||||||
|
},
|
||||||
%% Qos
|
%% Qos
|
||||||
{
|
{
|
||||||
expected_rule_with_qos_retain([2], all),
|
expected_rule_with_qos_retain([2], all),
|
||||||
|
@ -254,6 +262,12 @@ error_rich_action_cases() ->
|
||||||
<<"topics">> => [],
|
<<"topics">> => [],
|
||||||
<<"action">> => <<"publish">>,
|
<<"action">> => <<"publish">>,
|
||||||
<<"retain">> => 3
|
<<"retain">> => 3
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
<<"permission">> => <<"allow">>,
|
||||||
|
<<"topics">> => [],
|
||||||
|
<<"action">> => <<"publish">>,
|
||||||
|
<<"qos">> => [<<"3">>]
|
||||||
}
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,8 @@ run_checks(#{checks := Checks} = Case) ->
|
||||||
Checks
|
Checks
|
||||||
).
|
).
|
||||||
|
|
||||||
|
run_check(ClientInfo, Fun) when is_function(Fun, 0) ->
|
||||||
|
run_check(ClientInfo, Fun());
|
||||||
run_check(ClientInfo, {ExpectedPermission, Action, Topic}) ->
|
run_check(ClientInfo, {ExpectedPermission, Action, Topic}) ->
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
ExpectedPermission,
|
ExpectedPermission,
|
||||||
|
|
|
@ -715,13 +715,13 @@ parse_incoming(
|
||||||
NState = State#state{parse_state = NParseState},
|
NState = State#state{parse_state = NParseState},
|
||||||
parse_incoming(Rest, [Packet | Packets], NState)
|
parse_incoming(Rest, [Packet | Packets], NState)
|
||||||
catch
|
catch
|
||||||
error:Reason:Stk ->
|
error:Reason:Stack ->
|
||||||
?SLOG(error, #{
|
?SLOG(error, #{
|
||||||
msg => "parse_frame_failed",
|
msg => "parse_frame_failed",
|
||||||
at_state => ParseState,
|
at_state => ParseState,
|
||||||
input_bytes => Data,
|
input_bytes => Data,
|
||||||
reason => Reason,
|
reason => Reason,
|
||||||
stacktrace => Stk
|
stacktrace => Stack
|
||||||
}),
|
}),
|
||||||
{[{frame_error, Reason} | Packets], State}
|
{[{frame_error, Reason} | Packets], State}
|
||||||
end.
|
end.
|
||||||
|
|
|
@ -59,15 +59,14 @@ init_per_suite(Config) ->
|
||||||
application:load(emqx_gateway_coap),
|
application:load(emqx_gateway_coap),
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_gateway_schema, ?CONF_DEFAULT),
|
ok = emqx_common_test_helpers:load_config(emqx_gateway_schema, ?CONF_DEFAULT),
|
||||||
emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_authn, emqx_gateway]),
|
emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_authn, emqx_gateway]),
|
||||||
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
|
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_) ->
|
end_per_suite(_) ->
|
||||||
meck:unload(emqx_access_control),
|
|
||||||
{ok, _} = emqx:remove_config([<<"gateway">>, <<"coap">>]),
|
{ok, _} = emqx:remove_config([<<"gateway">>, <<"coap">>]),
|
||||||
emqx_mgmt_api_test_util:end_suite([emqx_gateway, emqx_authn, emqx_conf]).
|
emqx_mgmt_api_test_util:end_suite([emqx_gateway, emqx_authn, emqx_conf]).
|
||||||
|
|
||||||
init_per_testcase(t_connection_with_authn_failed, Config) ->
|
init_per_testcase(t_connection_with_authn_failed, Config) ->
|
||||||
|
ok = meck:new(emqx_access_control, [passthrough]),
|
||||||
ok = meck:expect(
|
ok = meck:expect(
|
||||||
emqx_access_control,
|
emqx_access_control,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -75,12 +74,11 @@ init_per_testcase(t_connection_with_authn_failed, Config) ->
|
||||||
),
|
),
|
||||||
Config;
|
Config;
|
||||||
init_per_testcase(_, Config) ->
|
init_per_testcase(_, Config) ->
|
||||||
|
ok = meck:new(emqx_access_control, [passthrough]),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_testcase(t_connection_with_authn_failed, Config) ->
|
|
||||||
ok = meck:delete(emqx_access_control, authenticate, 1),
|
|
||||||
Config;
|
|
||||||
end_per_testcase(_, Config) ->
|
end_per_testcase(_, Config) ->
|
||||||
|
ok = meck:unload(emqx_access_control),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
default_config() ->
|
default_config() ->
|
||||||
|
@ -213,6 +211,38 @@ t_publish(_) ->
|
||||||
end,
|
end,
|
||||||
with_connection(Topics, Action).
|
with_connection(Topics, Action).
|
||||||
|
|
||||||
|
t_publish_with_retain_qos_expiry(_) ->
|
||||||
|
_ = meck:expect(
|
||||||
|
emqx_access_control,
|
||||||
|
authorize,
|
||||||
|
fun(_, #{action_type := publish, qos := 1, retain := true}, _) ->
|
||||||
|
allow
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
Topics = [<<"abc">>],
|
||||||
|
Action = fun(Topic, Channel, Token) ->
|
||||||
|
Payload = <<"123">>,
|
||||||
|
URI = pubsub_uri(binary_to_list(Topic), Token) ++ "&retain=true&qos=1&expiry=60",
|
||||||
|
|
||||||
|
%% Sub topic first
|
||||||
|
emqx:subscribe(Topic),
|
||||||
|
|
||||||
|
Req = make_req(post, Payload),
|
||||||
|
{ok, changed, _} = do_request(Channel, URI, Req),
|
||||||
|
|
||||||
|
receive
|
||||||
|
{deliver, Topic, Msg} ->
|
||||||
|
?assertEqual(Topic, Msg#message.topic),
|
||||||
|
?assertEqual(Payload, Msg#message.payload)
|
||||||
|
after 500 ->
|
||||||
|
?assert(false)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
with_connection(Topics, Action),
|
||||||
|
|
||||||
|
_ = meck:validate(emqx_access_control).
|
||||||
|
|
||||||
t_subscribe(_) ->
|
t_subscribe(_) ->
|
||||||
%% can subscribe to a normal topic
|
%% can subscribe to a normal topic
|
||||||
Topics = [
|
Topics = [
|
||||||
|
|
|
@ -361,7 +361,7 @@ prepare_sql_to_conn(Conn, [{Key, SQL} | PrepareList]) when is_pid(Conn) ->
|
||||||
?SLOG(error, LogMeta#{result => failed, reason => Reason}),
|
?SLOG(error, LogMeta#{result => failed, reason => Reason}),
|
||||||
{error, undefined_table};
|
{error, undefined_table};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
% FIXME: we should try to differ on transient failers and
|
% FIXME: we should try to differ on transient failures and
|
||||||
% syntax failures. Retrying syntax failures is not very productive.
|
% syntax failures. Retrying syntax failures is not very productive.
|
||||||
?SLOG(error, LogMeta#{result => failed, reason => Reason}),
|
?SLOG(error, LogMeta#{result => failed, reason => Reason}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
|
|
Loading…
Reference in New Issue