235 lines
11 KiB
Erlang
235 lines
11 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% 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_auth_mysql_SUITE).
|
|
|
|
-compile(export_all).
|
|
-compile(nowarn_export_all).
|
|
|
|
-define(APP, emqx_auth_mysql).
|
|
|
|
-include_lib("emqx/include/emqx.hrl").
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include_lib("common_test/include/ct.hrl").
|
|
|
|
-define(DROP_ACL_TABLE, <<"DROP TABLE IF EXISTS mqtt_acl">>).
|
|
|
|
-define(CREATE_ACL_TABLE, <<"CREATE TABLE mqtt_acl ("
|
|
" id int(11) unsigned NOT NULL AUTO_INCREMENT,"
|
|
" allow int(1) DEFAULT NULL COMMENT '0: deny, 1: allow',"
|
|
" ipaddr varchar(60) DEFAULT NULL COMMENT 'IpAddress',"
|
|
" username varchar(100) DEFAULT NULL COMMENT 'Username',"
|
|
" clientid varchar(100) DEFAULT NULL COMMENT 'ClientId',"
|
|
" access int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub',"
|
|
" topic varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter',"
|
|
" PRIMARY KEY (`id`)"
|
|
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4">>).
|
|
|
|
-define(INIT_ACL, <<"INSERT INTO mqtt_acl (id, allow, ipaddr, username, clientid, access, topic)"
|
|
"VALUES (1,1,'127.0.0.1','u1','c1',1,'t1'),"
|
|
"(2,0,'127.0.0.1','u2','c2',1,'t1'),"
|
|
"(3,1,'10.10.0.110','u1','c1',1,'t1'),"
|
|
"(4,1,'127.0.0.1','u3','c3',3,'t1')">>).
|
|
|
|
-define(DROP_AUTH_TABLE, <<"DROP TABLE IF EXISTS `mqtt_user`">>).
|
|
|
|
-define(CREATE_AUTH_TABLE, <<"CREATE TABLE `mqtt_user` ("
|
|
"`id` int(11) unsigned NOT NULL AUTO_INCREMENT,"
|
|
"`username` varchar(100) DEFAULT NULL,"
|
|
"`password` varchar(100) DEFAULT NULL,"
|
|
"`salt` varchar(100) DEFAULT NULL,"
|
|
"`is_superuser` tinyint(1) DEFAULT 0,"
|
|
"`created` datetime DEFAULT NULL,"
|
|
"PRIMARY KEY (`id`),"
|
|
"UNIQUE KEY `mqtt_username` (`username`)"
|
|
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4">>).
|
|
|
|
-define(INIT_AUTH, <<"INSERT INTO mqtt_user (id, is_superuser, username, password, salt)"
|
|
"VALUES (1, 1, 'plain', 'plain', 'salt'),"
|
|
"(2, 0, 'md5', '1bc29b36f623ba82aaf6724fd3b16718', 'salt'),"
|
|
"(3, 0, 'sha', 'd8f4590320e1343a915b6394170650a8f35d6926', 'salt'),"
|
|
"(4, 0, 'sha256', '5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e', 'salt'),"
|
|
"(5, 0, 'pbkdf2_password', 'cdedb5281bb2f801565a1122b2563515', 'ATHENA.MIT.EDUraeburn'),"
|
|
"(6, 0, 'bcrypt_foo', '$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6', '$2a$12$sSS8Eg.ovVzaHzi1nUHYK.'),"
|
|
"(7, 0, 'bcrypt', '$2y$16$rEVsDarhgHYB0TGnDFJzyu5f.T.Ha9iXMTk9J36NCMWWM7O16qyaK', 'salt'),"
|
|
"(8, 0, 'bcrypt_wrong', '$2y$16$rEVsDarhgHYB0TGnDFJzyu', 'salt')">>).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Setups
|
|
%%--------------------------------------------------------------------
|
|
|
|
all() ->
|
|
emqx_ct:all(?MODULE).
|
|
|
|
init_per_suite(Cfg) ->
|
|
emqx_ct_helpers:start_apps([emqx_auth_mysql], fun set_special_configs/1),
|
|
init_mysql_data(),
|
|
Cfg.
|
|
|
|
end_per_suite(_) ->
|
|
deinit_mysql_data(),
|
|
emqx_ct_helpers:stop_apps([emqx_auth_mysql]),
|
|
ok.
|
|
|
|
set_special_configs(emqx) ->
|
|
application:set_env(emqx, allow_anonymous, false),
|
|
application:set_env(emqx, enable_acl_cache, false),
|
|
application:set_env(emqx, plugins_loaded_file,
|
|
emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins"));
|
|
|
|
set_special_configs(_App) ->
|
|
ok.
|
|
|
|
init_mysql_data() ->
|
|
{ok, Pid} = ecpool_worker:client(gproc_pool:pick_worker({ecpool, ?APP})),
|
|
%% Users
|
|
ok = mysql:query(Pid, ?DROP_AUTH_TABLE),
|
|
ok = mysql:query(Pid, ?CREATE_AUTH_TABLE),
|
|
ok = mysql:query(Pid, ?INIT_AUTH),
|
|
|
|
%% ACLs
|
|
ok = mysql:query(Pid, ?DROP_ACL_TABLE),
|
|
ok = mysql:query(Pid, ?CREATE_ACL_TABLE),
|
|
ok = mysql:query(Pid, ?INIT_ACL).
|
|
|
|
deinit_mysql_data() ->
|
|
{ok, Pid} = ecpool_worker:client(gproc_pool:pick_worker({ecpool, ?APP})),
|
|
ok = mysql:query(Pid, ?DROP_AUTH_TABLE),
|
|
ok = mysql:query(Pid, ?DROP_ACL_TABLE).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Test cases
|
|
%%--------------------------------------------------------------------
|
|
|
|
t_check_acl(_) ->
|
|
User0 = #{zone => external,peerhost => {127,0,0,1}},
|
|
allow = emqx_access_control:check_acl(User0, subscribe, <<"t1">>),
|
|
User1 = #{zone => external, clientid => <<"c1">>, username => <<"u1">>, peerhost => {127,0,0,1}},
|
|
User2 = #{zone => external, clientid => <<"c2">>, username => <<"u2">>, peerhost => {127,0,0,1}},
|
|
allow = emqx_access_control:check_acl(User1, subscribe, <<"t1">>),
|
|
deny = emqx_access_control:check_acl(User2, subscribe, <<"t1">>),
|
|
|
|
User3 = #{zone => external, peerhost => {10,10,0,110}, clientid => <<"c1">>, username => <<"u1">>},
|
|
User4 = #{zone => external, peerhost => {10,10,10,110}, clientid => <<"c1">>, username => <<"u1">>},
|
|
allow = emqx_access_control:check_acl(User3, subscribe, <<"t1">>),
|
|
allow = emqx_access_control:check_acl(User3, subscribe, <<"t1">>),
|
|
allow = emqx_access_control:check_acl(User3, subscribe, <<"t2">>),%% nomatch -> ignore -> emqx acl
|
|
allow = emqx_access_control:check_acl(User4, subscribe, <<"t1">>),%% nomatch -> ignore -> emqx acl
|
|
User5 = #{zone => external, peerhost => {127,0,0,1}, clientid => <<"c3">>, username => <<"u3">>},
|
|
allow = emqx_access_control:check_acl(User5, subscribe, <<"t1">>),
|
|
allow = emqx_access_control:check_acl(User5, publish, <<"t1">>).
|
|
|
|
t_acl_super(_Config) ->
|
|
reload([{password_hash, plain},
|
|
{auth_query, "select password from mqtt_user where username = '%u' limit 1"}]),
|
|
{ok, C} = emqtt:start_link([{host, "localhost"},
|
|
{clientid, <<"simpleClient">>},
|
|
{username, <<"plain">>},
|
|
{password, <<"plain">>}]),
|
|
{ok, _} = emqtt:connect(C),
|
|
timer:sleep(10),
|
|
emqtt:subscribe(C, <<"TopicA">>, qos2),
|
|
timer:sleep(1000),
|
|
emqtt:publish(C, <<"TopicA">>, <<"Payload">>, qos2),
|
|
timer:sleep(1000),
|
|
receive
|
|
{publish, #{payload := Payload}} ->
|
|
?assertEqual(<<"Payload">>, Payload)
|
|
after
|
|
1000 ->
|
|
ct:fail({receive_timeout, <<"Payload">>}),
|
|
ok
|
|
end,
|
|
emqtt:disconnect(C).
|
|
|
|
t_check_auth(_) ->
|
|
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
|
|
Md5 = #{clientid => <<"md5">>, username => <<"md5">>, zone => external},
|
|
Sha = #{clientid => <<"sha">>, username => <<"sha">>, zone => external},
|
|
Sha256 = #{clientid => <<"sha256">>, username => <<"sha256">>, zone => external},
|
|
Pbkdf2 = #{clientid => <<"pbkdf2_password">>, username => <<"pbkdf2_password">>, zone => external},
|
|
BcryptFoo = #{clientid => <<"bcrypt_foo">>, username => <<"bcrypt_foo">>, zone => external},
|
|
User1 = #{clientid => <<"bcrypt_foo">>, username => <<"user">>, zone => external},
|
|
Bcrypt = #{clientid => <<"bcrypt">>, username => <<"bcrypt">>, zone => external},
|
|
BcryptWrong = #{clientid => <<"bcrypt_wrong">>, username => <<"bcrypt_wrong">>, zone => external},
|
|
reload([{password_hash, plain}]),
|
|
{ok,#{is_superuser := true}} =
|
|
emqx_access_control:authenticate(Plain#{password => <<"plain">>}),
|
|
reload([{password_hash, md5}]),
|
|
{ok,#{is_superuser := false}} =
|
|
emqx_access_control:authenticate(Md5#{password => <<"md5">>}),
|
|
reload([{password_hash, sha}]),
|
|
{ok,#{is_superuser := false}} =
|
|
emqx_access_control:authenticate(Sha#{password => <<"sha">>}),
|
|
reload([{password_hash, sha256}]),
|
|
{ok,#{is_superuser := false}} =
|
|
emqx_access_control:authenticate(Sha256#{password => <<"sha256">>}),
|
|
reload([{password_hash, bcrypt}]),
|
|
{ok,#{is_superuser := false}} =
|
|
emqx_access_control:authenticate(Bcrypt#{password => <<"password">>}),
|
|
{error, not_authorized} =
|
|
emqx_access_control:authenticate(BcryptWrong#{password => <<"password">>}),
|
|
%%pbkdf2 sha
|
|
reload([{password_hash, {pbkdf2, sha, 1, 16}},
|
|
{auth_query, "select password, salt from mqtt_user where username = '%u' limit 1"}]),
|
|
{ok,#{is_superuser := false}} =
|
|
emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}),
|
|
reload([{password_hash, {salt, bcrypt}}]),
|
|
{ok,#{is_superuser := false}} =
|
|
emqx_access_control:authenticate(BcryptFoo#{password => <<"foo">>}),
|
|
{error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}),
|
|
{error, not_authorized} = emqx_access_control:authenticate(Bcrypt#{password => <<"password">>}).
|
|
|
|
t_comment_config(_) ->
|
|
application:stop(?APP),
|
|
[application:unset_env(?APP, Par) || Par <- [acl_query, auth_query]],
|
|
application:start(?APP).
|
|
|
|
t_placeholders(_) ->
|
|
ClientA = #{username => <<"plain">>, clientid => <<"plain">>, zone => external},
|
|
|
|
reload([{password_hash, plain},
|
|
{auth_query, "select password from mqtt_user where username = '%u' and 'a_cn_val' = '%C' limit 1"}]),
|
|
{error, not_authorized} =
|
|
emqx_access_control:authenticate(ClientA#{password => <<"plain">>}),
|
|
{error, not_authorized} =
|
|
emqx_access_control:authenticate(ClientA#{password => <<"plain">>, cn => undefined}),
|
|
{ok, _} =
|
|
emqx_access_control:authenticate(ClientA#{password => <<"plain">>, cn => <<"a_cn_val">>}),
|
|
|
|
reload([{auth_query, "select password from mqtt_user where username = '%c' and 'a_dn_val' = '%d' limit 1"}]),
|
|
{error, not_authorized} =
|
|
emqx_access_control:authenticate(ClientA#{password => <<"plain">>}),
|
|
{error, not_authorized} =
|
|
emqx_access_control:authenticate(ClientA#{password => <<"plain">>, dn => undefined}),
|
|
{ok, _} =
|
|
emqx_access_control:authenticate(ClientA#{password => <<"plain">>, dn => <<"a_dn_val">>}),
|
|
|
|
reload([{auth_query, "select password from mqtt_user where username = '%u' and '192.168.1.5' = '%a' limit 1"}]),
|
|
{error, not_authorized} =
|
|
emqx_access_control:authenticate(ClientA#{password => <<"plain">>}),
|
|
{ok, _} =
|
|
emqx_access_control:authenticate(ClientA#{password => <<"plain">>, peerhost => {192,168,1,5}}).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Internal funcs
|
|
%%--------------------------------------------------------------------
|
|
|
|
reload(Config) when is_list(Config) ->
|
|
application:stop(?APP),
|
|
[application:set_env(?APP, K, V) || {K, V} <- Config],
|
|
application:start(?APP).
|