Просмотр исходного кода

feat(authn): add enable_authn flag for listeners

Ilya Averyanov 3 лет назад
Родитель
Сommit
e381e3698f

+ 17 - 0
apps/emqx/i18n/emqx_schema_i18n.conf

@@ -2063,6 +2063,23 @@ Type of the rate limit.
     }
 }
 
+base_listener_enable_authn {
+    desc {
+        en: """
+Set <code>true</code> (default) to enable client authentication on this listener. 
+When set to <code>false</code> clients will be allowed to connect without authentication.
+"""
+        zh: """
+配置 <code>true</code> (默认值)启用客户端进行身份认证。
+配置 <code>false</code> 时,将不对客户端做任何认证。
+"""
+    }
+    label: {
+        en: "Enable authentication"
+        zh: "启用身份认证"
+    }
+}
+
 mqtt_listener_access_rules {
     desc {
         en: """

+ 2 - 0
apps/emqx/src/emqx_authentication.erl

@@ -214,6 +214,8 @@ when
 %% Authenticate
 %%------------------------------------------------------------------------------
 
+authenticate(#{enable_authn := false}, _AuthResult) ->
+    ignore;
 authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) ->
     case get_authenticators(Listener, global_chain(Protocol)) of
         {ok, ChainName, Authenticators} ->

+ 12 - 3
apps/emqx/src/emqx_channel.erl

@@ -102,7 +102,11 @@
 
 -type channel() :: #channel{}.
 
--type opts() :: #{zone := atom(), listener := {Type :: atom(), Name :: atom()}, atom() => term()}.
+-type opts() :: #{
+    zone := atom(),
+    listener := {Type :: atom(), Name :: atom()},
+    atom() => term()
+}.
 
 -type conn_state() :: idle | connecting | connected | reauthenticating | disconnected.
 
@@ -235,7 +239,11 @@ init(
         peername := {PeerHost, _Port},
         sockname := {_Host, SockPort}
     },
-    #{zone := Zone, limiter := LimiterCfg, listener := {Type, Listener}}
+    #{
+        zone := Zone,
+        limiter := LimiterCfg,
+        listener := {Type, Listener}
+    } = Opts
 ) ->
     Peercert = maps:get(peercert, ConnInfo, undefined),
     Protocol = maps:get(protocol, ConnInfo, mqtt),
@@ -256,7 +264,8 @@ init(
             username => undefined,
             mountpoint => MountPoint,
             is_bridge => false,
-            is_superuser => false
+            is_superuser => false,
+            enable_authn => maps:get(enable_authn, Opts, true)
         },
         Zone
     ),

+ 7 - 2
apps/emqx/src/emqx_listeners.erl

@@ -304,7 +304,8 @@ do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) when
             #{
                 listener => {Type, ListenerName},
                 zone => zone(Opts),
-                limiter => limiter(Opts)
+                limiter => limiter(Opts),
+                enable_authn => enable_authn(Opts)
             }
         ]}
     );
@@ -430,7 +431,8 @@ ws_opts(Type, ListenerName, Opts) ->
         {emqx_map_lib:deep_get([websocket, mqtt_path], Opts, "/mqtt"), emqx_ws_connection, #{
             zone => zone(Opts),
             listener => {Type, ListenerName},
-            limiter => limiter(Opts)
+            limiter => limiter(Opts),
+            enable_authn => enable_authn(Opts)
         }}
     ],
     Dispatch = cowboy_router:compile([{'_', WsPaths}]),
@@ -515,6 +517,9 @@ zone(Opts) ->
 limiter(Opts) ->
     maps:get(limiter, Opts, #{}).
 
+enable_authn(Opts) ->
+    maps:get(enable_authn, Opts, true).
+
 ssl_opts(Opts) ->
     maps:to_list(
         emqx_tls_lib:drop_tls13_for_old_otp(

+ 8 - 0
apps/emqx/src/emqx_schema.erl

@@ -1616,6 +1616,14 @@ base_listener(Bind) ->
                     desc => ?DESC(base_listener_limiter),
                     default => #{<<"connection">> => <<"default">>}
                 }
+            )},
+        {"enable_authn",
+            sc(
+                boolean(),
+                #{
+                    desc => ?DESC(base_listener_enable_authn),
+                    default => true
+                }
             )}
     ].
 

+ 0 - 54
apps/emqx/test/emqx_common_test_helpers.erl

@@ -38,8 +38,6 @@
 ]).
 
 -export([
-    change_emqx_opts/1,
-    change_emqx_opts/2,
     client_ssl/0,
     client_ssl/1,
     client_ssl_twoway/0,
@@ -320,58 +318,6 @@ wait_for(Fn, Ln, F, Timeout) ->
     {Pid, Mref} = erlang:spawn_monitor(fun() -> wait_loop(F, catch_call(F)) end),
     wait_for_down(Fn, Ln, Timeout, Pid, Mref, false).
 
-change_emqx_opts(SslType) ->
-    change_emqx_opts(SslType, []).
-
-change_emqx_opts(SslType, MoreOpts) ->
-    {ok, Listeners} = application:get_env(emqx, listeners),
-    NewListeners =
-        lists:map(
-            fun(Listener) ->
-                maybe_inject_listener_ssl_options(SslType, MoreOpts, Listener)
-            end,
-            Listeners
-        ),
-    emqx_conf:update([listeners], NewListeners, #{}).
-
-maybe_inject_listener_ssl_options(SslType, MoreOpts, {sll, Port, Opts}) ->
-    %% this clause is kept to be backward compatible
-    %% new config for listener is a map, old is a three-element tuple
-    {ssl, Port, inject_listener_ssl_options(SslType, Opts, MoreOpts)};
-maybe_inject_listener_ssl_options(SslType, MoreOpts, #{proto := ssl, opts := Opts} = Listener) ->
-    Listener#{opts := inject_listener_ssl_options(SslType, Opts, MoreOpts)};
-maybe_inject_listener_ssl_options(_SslType, _MoreOpts, Listener) ->
-    Listener.
-
-inject_listener_ssl_options(SslType, Opts, MoreOpts) ->
-    SslOpts = proplists:get_value(ssl_options, Opts),
-    Keyfile = app_path(emqx, filename:join(["etc", "certs", "key.pem"])),
-    Certfile = app_path(emqx, filename:join(["etc", "certs", "cert.pem"])),
-    TupleList1 = lists:keyreplace(keyfile, 1, SslOpts, {keyfile, Keyfile}),
-    TupleList2 = lists:keyreplace(certfile, 1, TupleList1, {certfile, Certfile}),
-    TupleList3 =
-        case SslType of
-            ssl_twoway ->
-                CAfile = app_path(emqx, proplists:get_value(cacertfile, ?MQTT_SSL_TWOWAY)),
-                MutSslList = lists:keyreplace(
-                    cacertfile, 1, ?MQTT_SSL_TWOWAY, {cacertfile, CAfile}
-                ),
-                lists:merge(TupleList2, MutSslList);
-            _ ->
-                lists:filter(
-                    fun
-                        ({cacertfile, _}) -> false;
-                        ({verify, _}) -> false;
-                        ({fail_if_no_peer_cert, _}) -> false;
-                        (_) -> true
-                    end,
-                    TupleList2
-                )
-        end,
-    TupleList4 = emqx_misc:merge_opts(TupleList3, proplists:get_value(ssl_options, MoreOpts, [])),
-    NMoreOpts = emqx_misc:merge_opts(MoreOpts, [{ssl_options, TupleList4}]),
-    emqx_misc:merge_opts(Opts, NMoreOpts).
-
 flush() ->
     flush([]).
 

+ 103 - 0
apps/emqx_authn/test/emqx_authn_enable_flag_SUITE.erl

@@ -0,0 +1,103 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022 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_enable_flag_SUITE).
+
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-include("emqx_authn.hrl").
+
+-define(PATH, [?CONF_NS_ATOM]).
+
+-include_lib("eunit/include/eunit.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]),
+    Config.
+
+end_per_suite(_) ->
+    emqx_common_test_helpers:stop_apps([emqx_authn, emqx_conf]),
+    ok.
+
+init_per_testcase(_Case, Config) ->
+    AuthnConfig = #{
+        <<"mechanism">> => <<"password_based">>,
+        <<"backend">> => <<"built_in_database">>,
+        <<"user_id_type">> => <<"clientid">>
+    },
+    emqx:update_config(
+        ?PATH,
+        {create_authenticator, ?GLOBAL, AuthnConfig}
+    ),
+
+    emqx_conf:update(
+        [listeners, tcp, listener_authn_enabled], {create, listener_mqtt_tcp_conf(18830, true)}, #{}
+    ),
+    emqx_conf:update(
+        [listeners, tcp, listener_authn_disabled],
+        {create, listener_mqtt_tcp_conf(18831, false)},
+        #{}
+    ),
+    Config.
+
+end_per_testcase(_Case, Config) ->
+    emqx_authn_test_lib:delete_authenticators(
+        ?PATH,
+        ?GLOBAL
+    ),
+    emqx_conf:remove(
+        [listeners, tcp, listener_authn_enabled], #{}
+    ),
+    emqx_conf:remove(
+        [listeners, tcp, listener_authn_disabled], #{}
+    ),
+    Config.
+
+listener_mqtt_tcp_conf(Port, EnableAuthn) ->
+    #{
+        acceptors => 16,
+        zone => default,
+        access_rules => ["allow all"],
+        bind => {{0, 0, 0, 0}, Port},
+        max_connections => 1024000,
+        mountpoint => <<>>,
+        proxy_protocol => false,
+        proxy_protocol_timeout => 3000,
+        enable_authn => EnableAuthn
+    }.
+
+t_enable_authn(_Config) ->
+    %% enable_authn set to false, we connect successfully
+    {ok, ConnPid0} = emqtt:start_link([{port, 18831}, {clientid, <<"clientid">>}]),
+    ?assertMatch(
+        {ok, _},
+        emqtt:connect(ConnPid0)
+    ),
+    ok = emqtt:disconnect(ConnPid0),
+
+    process_flag(trap_exit, true),
+
+    %% enable_authn set to true, we go to the set up authn and fail
+    {ok, ConnPid1} = emqtt:start_link([{port, 18830}, {clientid, <<"clientid">>}]),
+    ?assertMatch(
+        {error, {unauthorized_client, _}},
+        emqtt:connect(ConnPid1)
+    ),
+    ok.

+ 9 - 0
apps/emqx_gateway/i18n/emqx_gateway_schema_i18n.conf

@@ -589,6 +589,15 @@ See: https://erlang.org/doc/man/inet.html#setopts-2"""
         }
     }
 
+    gateway_common_listener_enable_authn {
+        desc {
+            en: """Set <code>true</code> (default) to enable client authentication on this listener. 
+When set to <code>false</code> clients will be allowed to connect without authentication."""
+            zh: """配置 <code>true</code> (默认值)启用客户端进行身份认证。
+配置 <code>false</code> 时,将不对客户端做任何认证。"""
+        }
+    }
+
     gateway_common_listener_mountpoint {
         desc {
             en: """When publishing or subscribing, prefix all topics with a mountpoint string.

+ 2 - 0
apps/emqx_gateway/src/coap/emqx_coap_channel.erl

@@ -131,6 +131,7 @@ init(
 ) ->
     Peercert = maps:get(peercert, ConnInfo, undefined),
     Mountpoint = maps:get(mountpoint, Config, <<>>),
+    EnableAuthn = maps:get(enable_authn, Config, true),
     ListenerId =
         case maps:get(listener, Config, undefined) of
             undefined -> undefined;
@@ -148,6 +149,7 @@ init(
             username => undefined,
             is_bridge => false,
             is_superuser => false,
+            enable_authn => EnableAuthn,
             mountpoint => Mountpoint
         }
     ),

+ 2 - 6
apps/emqx_gateway/src/emqx_gateway_ctx.erl

@@ -26,11 +26,9 @@
 %% configuration, register devices and other common operations.
 %%
 -type context() ::
-    %% Gateway Name
     #{
+        %% Gateway Name
         gwname := gateway_name(),
-        %% Authentication chains
-        auth := [emqx_authentication:chain_name()],
         %% The ConnectionManager PID
         cm := pid()
     }.
@@ -67,9 +65,7 @@
 -spec authenticate(context(), emqx_types:clientinfo()) ->
     {ok, emqx_types:clientinfo()}
     | {error, any()}.
-authenticate(_Ctx = #{auth := _ChainNames}, ClientInfo0) when
-    is_list(_ChainNames)
-->
+authenticate(_Ctx, ClientInfo0) ->
     ClientInfo = ClientInfo0#{zone => default},
     case emqx_access_control:authenticate(ClientInfo) of
         {ok, _} ->

+ 8 - 0
apps/emqx_gateway/src/emqx_gateway_schema.erl

@@ -649,6 +649,14 @@ common_listener_opts() ->
                 }
             )},
         {?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM, authentication_schema()},
+        {"enable_authn",
+            sc(
+                boolean(),
+                #{
+                    desc => ?DESC(gateway_common_listener_enable_authn),
+                    default => true
+                }
+            )},
         {mountpoint,
             sc(
                 binary(),

+ 6 - 1
apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl

@@ -157,7 +157,12 @@ init(
             undefined -> undefined;
             {GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
         end,
-    ClientInfo = maps:put(listener, ListenerId, default_clientinfo(ConnInfo)),
+    EnableAuthn = maps:get(enable_authn, Options, true),
+    DefaultClientInfo = default_clientinfo(ConnInfo),
+    ClientInfo = DefaultClientInfo#{
+        listener => ListenerId,
+        enable_authn => EnableAuthn
+    },
     Channel = #channel{
         ctx = Ctx,
         gcli = #{channel => GRpcChann, pool_name => PoolName},

+ 2 - 0
apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl

@@ -128,6 +128,7 @@ init(
             undefined -> undefined;
             {GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
         end,
+    EnableAuthn = maps:get(enable_authn, Config, true),
     ClientInfo = set_peercert_infos(
         Peercert,
         #{
@@ -140,6 +141,7 @@ init(
             clientid => undefined,
             is_bridge => false,
             is_superuser => false,
+            enable_authn => EnableAuthn,
             mountpoint => Mountpoint
         }
     ),

+ 2 - 0
apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl

@@ -156,6 +156,7 @@ init(
             undefined -> undefined;
             {GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
         end,
+    EnableAuthn = maps:get(enable_authn, Option, true),
     ClientInfo = set_peercert_infos(
         Peercert,
         #{
@@ -168,6 +169,7 @@ init(
             username => undefined,
             is_bridge => false,
             is_superuser => false,
+            enable_authn => EnableAuthn,
             mountpoint => Mountpoint
         }
     ),

+ 2 - 0
apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl

@@ -127,6 +127,7 @@ init(
             undefined -> undefined;
             {GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
         end,
+    EnableAuthn = maps:get(enable_authn, Option, true),
     ClientInfo = setting_peercert_infos(
         Peercert,
         #{
@@ -139,6 +140,7 @@ init(
             username => undefined,
             is_bridge => false,
             is_superuser => false,
+            enable_authn => EnableAuthn,
             mountpoint => Mountpoint
         }
     ),

+ 36 - 0
apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl

@@ -109,6 +109,12 @@ t_case_coap(_) ->
         Prefix ++
             "/connection?clientid=client1&username=bad&password=bad",
     Login(LeftUrl, ?checkMatch({error, bad_request, _Data})),
+
+    disable_authn(coap, udp, default),
+    NowRightUrl =
+        Prefix ++
+            "/connection?clientid=client1&username=bad&password=bad",
+    Login(NowRightUrl, ?checkMatch({ok, created, _Data})),
     ok.
 
 -record(coap_content, {content_format, payload = <<>>}).
@@ -155,6 +161,11 @@ t_case_lwm2m(_) ->
 
     NoInfoUrl = "coap://127.0.0.1:~b/rd?ep=~ts&lt=345&lwm2m=1",
     Login(NoInfoUrl, MakeCheker(ack, {error, bad_request})),
+
+    disable_authn(lwm2m, udp, default),
+    NowRightUrl = "coap://127.0.0.1:~b/rd?ep=~ts&lt=345&lwm2m=1&imei=bad&password=bad",
+    Login(NowRightUrl, MakeCheker(ack, {ok, created})),
+
     ok.
 
 -define(SN_CONNACK, 16#05).
@@ -182,6 +193,9 @@ t_case_mqttsn(_) ->
     end,
     Login(<<"badadmin">>, <<"badpassowrd">>, <<3, ?SN_CONNACK, 16#80>>),
     Login(<<"admin">>, <<"public">>, <<3, ?SN_CONNACK, 0>>),
+
+    disable_authn(mqttsn, udp, default),
+    Login(<<"badadmin">>, <<"badpassowrd">>, <<3, ?SN_CONNACK, 0>>),
     ok.
 
 t_case_stomp(_) ->
@@ -220,6 +234,15 @@ t_case_stomp(_) ->
         ?assertEqual(<<"Login Failed: not_authorized">>, Mod:get_field(body, Frame))
     end),
 
+    disable_authn(stomp, tcp, default),
+    Login(
+        <<"bad">>,
+        <<"bad">>,
+        ?FUNCTOR(
+            Frame,
+            ?assertEqual(<<"CONNECTED">>, Mod:get_field(command, Frame))
+        )
+    ),
     ok.
 
 t_case_exproto(_) ->
@@ -249,5 +272,18 @@ t_case_exproto(_) ->
     end,
     Login(<<"admin">>, <<"public">>, SvrMod:frame_connack(0)),
     Login(<<"bad">>, <<"bad">>, SvrMod:frame_connack(1)),
+
+    disable_authn(exproto, tcp, default),
+    Login(<<"bad">>, <<"bad">>, SvrMod:frame_connack(0)),
+
     SvrMod:stop(Svrs),
     ok.
+
+disable_authn(GwName, Type, Name) ->
+    RawCfg = emqx_conf:get_raw([gateway, GwName], #{}),
+    ListenerCfg = emqx_map_lib:deep_get(
+        [<<"listeners">>, atom_to_binary(Type), atom_to_binary(Name)], RawCfg
+    ),
+    {ok, _} = emqx_gateway_conf:update_listener(GwName, {Type, Name}, ListenerCfg#{
+        <<"enable_authn">> => false
+    }).

+ 1 - 1
apps/emqx_gateway/test/emqx_gateway_ctx_SUITE.erl

@@ -50,7 +50,7 @@ end_per_suite(_Conf) ->
 %%--------------------------------------------------------------------
 
 t_authenticate(_) ->
-    Ctx = #{gwname => mqttsn, auth => [], cm => self()},
+    Ctx = #{gwname => mqttsn, cm => self()},
     Info1 = #{
         mountpoint => undefined,
         clientid => <<"user1">>