Jelajahi Sumber

chore(authn): add SCRAM mechanism tests

Ilya Averyanov 4 tahun lalu
induk
melakukan
4580c03ebc

+ 2 - 2
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},
              <<AckFlags:8, ReasonCode:8, Rest/binary>>, #{version := Ver}) ->

+ 14 - 9
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).

+ 1 - 1
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) ->

+ 115 - 0
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}]).

+ 373 - 0
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.