diff --git a/apps/emqx/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl index 2fe1b6d1a..647d076d1 100644 --- a/apps/emqx/src/emqx_frame.erl +++ b/apps/emqx/src/emqx_frame.erl @@ -248,8 +248,8 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> }, {ConnPacket1, Rest5} = parse_will_message(ConnPacket, Rest4), {Username, Rest6} = parse_utf8_string(Rest5, bool(UsernameFlag)), - {Passsword, <<>>} = parse_utf8_string(Rest6, bool(PasswordFlag)), - ConnPacket1#mqtt_packet_connect{username = Username, password = Passsword}; + {Password, <<>>} = parse_utf8_string(Rest6, bool(PasswordFlag)), + ConnPacket1#mqtt_packet_connect{username = Username, password = Password}; parse_packet(#mqtt_packet_header{type = ?CONNACK}, <>, #{version := Ver}) -> diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index 5a477c7e0..f02655462 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -137,10 +137,7 @@ authenticate(_Credential, _State) -> ignore. destroy(#{user_group := UserGroup}) -> - MatchSpec = ets:fun2ms( - fun(#user_info{user_id = {Group, _}} = User) when Group =:= UserGroup -> - User - end), + MatchSpec = group_match_spec(UserGroup), trans( fun() -> ok = lists:foreach(fun(UserInfo) -> @@ -205,16 +202,16 @@ lookup_user(UserID, #{user_group := UserGroup}) -> end. list_users(PageParams, #{user_group := UserGroup}) -> - MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_', '_'}, [], ['$_']}], + MatchSpec = group_match_spec(UserGroup), {ok, emqx_mgmt_api:paginate(?TAB, MatchSpec, PageParams, ?FORMAT_FUN)}. %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ -ensure_auth_method('SCRAM-SHA-256', #{algorithm := sha256}) -> +ensure_auth_method(<<"SCRAM-SHA-256">>, #{algorithm := sha256}) -> true; -ensure_auth_method('SCRAM-SHA-512', #{algorithm := sha512}) -> +ensure_auth_method(<<"SCRAM-SHA-512">>, #{algorithm := sha512}) -> true; ensure_auth_method(_, _) -> false. @@ -228,8 +225,10 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S #{iteration_count => IterationCount, retrieve => RetrieveFun} ) of - {cotinue, ServerFirstMessage, Cache} -> - {cotinue, ServerFirstMessage, Cache}; + {continue, ServerFirstMessage, Cache} -> + {continue, ServerFirstMessage, Cache}; + ignore -> + ignore; {error, _Reason} -> {error, not_authorized} end. @@ -280,3 +279,9 @@ trans(Fun, Args) -> format_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) -> #{user_id => UserID, is_superuser => IsSuperuser}. + +group_match_spec(UserGroup) -> + ets:fun2ms( + fun(#user_info{user_id = {Group, _}} = User) when Group =:= UserGroup -> + User + end). diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index b5bca513c..b9eadbef6 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -37,7 +37,7 @@ end_per_suite(_) -> ok. init_per_testcase(_Case, Config) -> - mnesia:clear_table(emqx_authn_mnesia), + mria:clear_table(emqx_authn_mnesia), Config. end_per_testcase(_Case, Config) -> diff --git a/apps/emqx_authn/test/emqx_authn_mqtt_test_client.erl b/apps/emqx_authn/test/emqx_authn_mqtt_test_client.erl new file mode 100644 index 000000000..a217cb96d --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_mqtt_test_client.erl @@ -0,0 +1,115 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 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_authn_mqtt_test_client). + +-behaviour(gen_server). + +-include_lib("emqx/include/emqx_mqtt.hrl"). + +%% API +-export([start_link/2, + stop/1]). + +-export([send/2]). + +%% gen_server callbacks + +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2]). + +-define(TIMEOUT, 1000). +-define(TCP_OPTIONS, [binary, {packet, raw}, {active, once}, + {nodelay, true}]). + +-define(PARSE_OPTIONS, + #{strict_mode => false, + max_size => ?MAX_PACKET_SIZE, + version => ?MQTT_PROTO_V5 + }). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +start_link(Host, Port) -> + gen_server:start_link(?MODULE, [Host, Port, self()], []). + +stop(Pid) -> + gen_server:call(Pid, stop). + +send(Pid, Packet) -> + gen_server:call(Pid, {send, Packet}). + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init([Host, Port, Owner]) -> + {ok, Socket} = gen_tcp:connect(Host, Port, ?TCP_OPTIONS, ?TIMEOUT), + {ok, #{owner => Owner, + socket => Socket, + parse_state => emqx_frame:initial_parse_state(?PARSE_OPTIONS) + }}. + +handle_info({tcp, _Sock, Data}, #{parse_state := PSt, + owner := Owner, + socket := Socket} = St) -> + {NewPSt, Packets} = process_incoming(PSt, Data, []), + ok = deliver(Owner, Packets), + ok = run_sock(Socket), + {noreply, St#{parse_state => NewPSt}}; + +handle_info({tcp_closed, _Sock}, St) -> + {stop, normal, St}. + +handle_call({send, Packet}, _From, #{socket := Socket} = St) -> + ok = gen_tcp:send(Socket, emqx_frame:serialize(Packet, ?MQTT_PROTO_V5)), + {reply, ok, St}; + +handle_call(stop, _From, #{socket := Socket} = St) -> + ok = gen_tcp:close(Socket), + {stop, normal, ok, St}. + +handle_cast(_, St) -> + {noreply, St}. + +terminate(_Reason, _St) -> + ok. + +%%-------------------------------------------------------------------- +%% internal functions +%%-------------------------------------------------------------------- + +process_incoming(PSt, Data, Packets) -> + case emqx_frame:parse(Data, PSt) of + {more, NewPSt} -> + {NewPSt, lists:reverse(Packets)}; + {ok, Packet, Rest, NewPSt} -> + process_incoming(NewPSt, Rest, [Packet | Packets]) + end. + +deliver(_Owner, []) -> ok; +deliver(Owner, [Packet | Packets]) -> + Owner ! {packet, Packet}, + deliver(Owner, Packets). + + +run_sock(Socket) -> + inet:setopts(Socket, [{active, once}]). diff --git a/apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl new file mode 100644 index 000000000..3504b88cc --- /dev/null +++ b/apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl @@ -0,0 +1,373 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 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_enhanced_authn_scram_mnesia_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include("emqx_authn.hrl"). + +-define(PATH, [authentication]). + +-define(USER_MAP, #{user_id := _, + is_superuser := _}). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_common_test_helpers:start_apps([emqx_authn]), + Config. + +end_per_suite(_Config) -> + ok = emqx_common_test_helpers:stop_apps([emqx_authn]). + +init_per_testcase(_Case, Config) -> + mria:clear_table(emqx_enhanced_authn_scram_mnesia), + emqx_authn_test_lib:delete_authenticators( + [authentication], + ?GLOBAL), + Config. + +end_per_testcase(_Case, Config) -> + Config. + +%%------------------------------------------------------------------------------ +%% Tests +%%------------------------------------------------------------------------------ + +t_create(_Config) -> + ValidConfig = #{ + <<"mechanism">> => <<"scram">>, + <<"backend">> => <<"built-in-database">>, + <<"algorithm">> => <<"sha512">>, + <<"iteration_count">> => <<"4096">> + }, + + {ok, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, ValidConfig}), + + {ok, [#{provider := emqx_enhanced_authn_scram_mnesia}]} + = emqx_authentication:list_authenticators(?GLOBAL). + +t_create_invalid(_Config) -> + InvalidConfig = #{ + <<"mechanism">> => <<"scram">>, + <<"backend">> => <<"built-in-database">>, + <<"algorithm">> => <<"sha271828">>, + <<"iteration_count">> => <<"4096">> + }, + + {error, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, InvalidConfig}), + + {ok, []} = emqx_authentication:list_authenticators(?GLOBAL). + +t_authenticate(_Config) -> + Algorithm = sha512, + Username = <<"u">>, + Password = <<"p">>, + + init_auth(Username, Password, Algorithm), + + {ok, Pid} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883), + + ClientFirstMessage = esasl_scram:client_first_message(Username), + + ConnectPacket = ?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ?MQTT_PROTO_V5, + properties = #{ + 'Authentication-Method' => <<"SCRAM-SHA-512">>, + 'Authentication-Data' => ClientFirstMessage + } + }), + + ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket), + + ?AUTH_PACKET( + ?RC_CONTINUE_AUTHENTICATION, + #{'Authentication-Data' := ServerFirstMessage}) = receive_packet(), + + {continue, ClientFinalMessage, ClientCache} = + esasl_scram:check_server_first_message( + ServerFirstMessage, + #{client_first_message => ClientFirstMessage, + password => Password, + algorithm => Algorithm} + ), + + AuthContinuePacket = ?AUTH_PACKET( + ?RC_CONTINUE_AUTHENTICATION, + #{'Authentication-Method' => <<"SCRAM-SHA-512">>, + 'Authentication-Data' => ClientFinalMessage}), + + ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket), + + ?CONNACK_PACKET( + ?RC_SUCCESS, + _, + #{'Authentication-Data' := ServerFinalMessage}) = receive_packet(), + + ok = esasl_scram:check_server_final_message( + ServerFinalMessage, ClientCache#{algorithm => Algorithm} + ). + +t_authenticate_bad_username(_Config) -> + Algorithm = sha512, + Username = <<"u">>, + Password = <<"p">>, + + init_auth(Username, Password, Algorithm), + + {ok, Pid} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883), + + ClientFirstMessage = esasl_scram:client_first_message(<<"badusername">>), + + ConnectPacket = ?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ?MQTT_PROTO_V5, + properties = #{ + 'Authentication-Method' => <<"SCRAM-SHA-512">>, + 'Authentication-Data' => ClientFirstMessage + } + }), + + ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket), + + ?CONNACK_PACKET(?RC_NOT_AUTHORIZED) = receive_packet(). + +t_authenticate_bad_password(_Config) -> + Algorithm = sha512, + Username = <<"u">>, + Password = <<"p">>, + + init_auth(Username, Password, Algorithm), + + {ok, Pid} = emqx_authn_mqtt_test_client:start_link("127.0.0.1", 1883), + + ClientFirstMessage = esasl_scram:client_first_message(Username), + + ConnectPacket = ?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ?MQTT_PROTO_V5, + properties = #{ + 'Authentication-Method' => <<"SCRAM-SHA-512">>, + 'Authentication-Data' => ClientFirstMessage + } + }), + + ok = emqx_authn_mqtt_test_client:send(Pid, ConnectPacket), + + ?AUTH_PACKET( + ?RC_CONTINUE_AUTHENTICATION, + #{'Authentication-Data' := ServerFirstMessage}) = receive_packet(), + + {continue, ClientFinalMessage, _ClientCache} = + esasl_scram:check_server_first_message( + ServerFirstMessage, + #{client_first_message => ClientFirstMessage, + password => <<"badpassword">>, + algorithm => Algorithm} + ), + + AuthContinuePacket = ?AUTH_PACKET( + ?RC_CONTINUE_AUTHENTICATION, + #{'Authentication-Method' => <<"SCRAM-SHA-512">>, + 'Authentication-Data' => ClientFinalMessage}), + + ok = emqx_authn_mqtt_test_client:send(Pid, AuthContinuePacket), + + ?CONNACK_PACKET(?RC_NOT_AUTHORIZED) = receive_packet(). + +t_destroy(_) -> + Config = config(), + OtherId = list_to_binary([<<"id-other">>]), + {ok, State0} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), + {ok, StateOther} = emqx_enhanced_authn_scram_mnesia:create(OtherId, Config), + + User = #{user_id => <<"u">>, password => <<"p">>}, + + {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State0), + {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, StateOther), + + {ok, _} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State0), + {ok, _} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, StateOther), + + ok = emqx_enhanced_authn_scram_mnesia:destroy(State0), + + {ok, State1} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), + {error,not_found} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State1), + {ok, _} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, StateOther). + +t_add_user(_) -> + Config = config(), + {ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), + + User = #{user_id => <<"u">>, password => <<"p">>}, + {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State), + {error, already_exist} = emqx_enhanced_authn_scram_mnesia:add_user(User, State). + +t_delete_user(_) -> + Config = config(), + {ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), + + {error, not_found} = emqx_enhanced_authn_scram_mnesia:delete_user(<<"u">>, State), + User = #{user_id => <<"u">>, password => <<"p">>}, + {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State), + + ok = emqx_enhanced_authn_scram_mnesia:delete_user(<<"u">>, State), + {error, not_found} = emqx_enhanced_authn_scram_mnesia:delete_user(<<"u">>, State). + +t_update_user(_) -> + Config = config(), + {ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), + + User = #{user_id => <<"u">>, password => <<"p">>}, + {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(User, State), + {ok, #{is_superuser := false}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State), + + {ok, + #{user_id := <<"u">>, + is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:update_user( + <<"u">>, + #{password => <<"p1">>, is_superuser => true}, + State), + + {ok, #{is_superuser := true}} = emqx_enhanced_authn_scram_mnesia:lookup_user(<<"u">>, State). + +t_list_users(_) -> + Config = config(), + {ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), + + Users = [#{user_id => <<"u1">>, password => <<"p">>}, + #{user_id => <<"u2">>, password => <<"p">>}, + #{user_id => <<"u3">>, password => <<"p">>}], + + lists:foreach( + fun(U) -> {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(U, State) end, + Users), + + {ok, + #{data := [?USER_MAP, ?USER_MAP], + meta := #{page := 1, limit := 2, count := 3}}} = emqx_enhanced_authn_scram_mnesia:list_users( + #{<<"page">> => 1, <<"limit">> => 2}, + State), + {ok, + #{data := [?USER_MAP], + meta := #{page := 2, limit := 2, count := 3}}} = emqx_enhanced_authn_scram_mnesia:list_users( + #{<<"page">> => 2, <<"limit">> => 2}, + State). + +t_is_superuser(_Config) -> + ok = test_is_superuser(#{is_superuser => false}, false), + ok = test_is_superuser(#{is_superuser => true}, true), + ok = test_is_superuser(#{}, false). + +test_is_superuser(UserInfo, ExpectedIsSuperuser) -> + Config = config(), + {ok, State} = emqx_enhanced_authn_scram_mnesia:create(<<"id">>, Config), + + Username = <<"u">>, + Password = <<"p">>, + + UserInfo0 = UserInfo#{user_id => Username, + password => Password}, + + {ok, _} = emqx_enhanced_authn_scram_mnesia:add_user(UserInfo0, State), + + ClientFirstMessage = esasl_scram:client_first_message(Username), + + {continue, ServerFirstMessage, ServerCache} + = emqx_enhanced_authn_scram_mnesia:authenticate( + #{auth_method => <<"SCRAM-SHA-512">>, + auth_data => ClientFirstMessage, + auth_cache => #{} + }, + State), + + {continue, ClientFinalMessage, ClientCache} = + esasl_scram:check_server_first_message( + ServerFirstMessage, + #{client_first_message => ClientFirstMessage, + password => Password, + algorithm => sha512} + ), + + {ok, UserInfo1, ServerFinalMessage} + = emqx_enhanced_authn_scram_mnesia:authenticate( + #{auth_method => <<"SCRAM-SHA-512">>, + auth_data => ClientFinalMessage, + auth_cache => ServerCache + }, + State), + + ok = esasl_scram:check_server_final_message( + ServerFinalMessage, ClientCache#{algorithm => sha512} + ), + + ?assertMatch(#{is_superuser := ExpectedIsSuperuser}, UserInfo1), + + ok = emqx_enhanced_authn_scram_mnesia:destroy(State). + + +%%------------------------------------------------------------------------------ +%% Helpers +%%------------------------------------------------------------------------------ + +config() -> + #{ + mechanism => <<"scram">>, + backend => <<"built-in-database">>, + algorithm => sha512, + iteration_count => 4096 + }. + +raw_config(Algorithm) -> + #{ + <<"mechanism">> => <<"scram">>, + <<"backend">> => <<"built-in-database">>, + <<"algorithm">> => atom_to_binary(Algorithm), + <<"iteration_count">> => <<"4096">> + }. + +init_auth(Username, Password, Algorithm) -> + Config = raw_config(Algorithm), + + {ok, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, Config}), + + {ok, [#{state := State}]} = emqx_authentication:list_authenticators(?GLOBAL), + + emqx_enhanced_authn_scram_mnesia:add_user( + #{user_id => Username, password => Password}, + State). + +receive_packet() -> + receive + {packet, Packet} -> + ct:pal("Delivered packet: ~p", [Packet]), + Packet + after 1000 -> + ct:fail("Deliver timeout") + end.