emqx/apps/emqx_auth_mysql/test/emqx_authz_mysql_SUITE.erl

456 lines
17 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-2024 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_mysql_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include("../../emqx_connector/include/emqx_connector.hrl").
-include_lib("emqx_auth/include/emqx_authz.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(MYSQL_HOST, "mysql").
-define(MYSQL_RESOURCE, <<"emqx_authz_mysql_SUITE">>).
all() ->
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) ->
case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
true ->
Apps = emqx_cth_suite:start(
[
emqx,
{emqx_conf,
"authorization.no_match = deny, authorization.cache.enable = false"},
emqx_auth,
emqx_auth_mysql
],
#{work_dir => ?config(priv_dir, Config)}
),
ok = create_mysql_resource(),
[{suite_apps, Apps} | Config];
false ->
{skip, no_mysql}
end.
end_per_suite(Config) ->
ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
ok = emqx_cth_suite:stop(?config(suite_apps, Config)).
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.
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
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(
raw_mysql_authz_config(),
#{<<"server">> => <<"255.255.255.255:33333">>}
),
{ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
[_] = emqx_authz:lookup().
%%------------------------------------------------------------------------------
%% Cases
%%------------------------------------------------------------------------------
cases() ->
[
#{
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))",
"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 => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255))"
],
query => "SELECT permission, action, topic FRO",
checks => [
{deny, ?AUTHZ_PUBLISH, <<"a">>}
]
},
#{
name => runtime_error,
setup => [
"CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
"permission VARCHAR(255), action VARCHAR(255))"
],
query =>
"SELECT permission, action, topic FROM acl WHERE username = ${username}",
checks => [
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">>}
]
}
].
%%------------------------------------------------------------------------------
%% 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">>,
<<"type">> => <<"mysql">>,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"root">>,
<<"password">> => <<"public">>,
<<"query">> => <<
"SELECT permission, action, topic "
"FROM acl WHERE username = ${username}"
>>,
<<"server">> => <<?MYSQL_HOST>>,
<<"pool_size">> => <<"1">>
}.
q(Sql) ->
emqx_resource:simple_sync_query(
?MYSQL_RESOURCE,
{sql, Sql}
).
q(Sql, Params) ->
emqx_resource:simple_sync_query(
?MYSQL_RESOURCE,
{sql, Sql, Params}
).
drop_table() ->
ok = q("DROP TABLE IF EXISTS acl").
setup_config(SpecialParams) ->
emqx_authz_test_lib:setup_config(
raw_mysql_authz_config(),
SpecialParams
).
mysql_config() ->
#{
auto_reconnect => true,
database => <<"mqtt">>,
username => <<"root">>,
password => <<"public">>,
pool_size => 1,
server => <<?MYSQL_HOST>>,
ssl => #{enable => false}
}.
start_apps(Apps) ->
lists:foreach(fun application:ensure_all_started/1, Apps).
stop_apps(Apps) ->
lists:foreach(fun application:stop/1, Apps).
create_mysql_resource() ->
{ok, _} = emqx_resource:create_local(
?MYSQL_RESOURCE,
?AUTHZ_RESOURCE_GROUP,
emqx_mysql,
mysql_config(),
#{}
),
ok.