Bläddra i källkod

feat(authn): support quick deny anonymous

firest 3 år sedan
förälder
incheckning
2bc8b00419

+ 10 - 4
apps/emqx/i18n/emqx_schema_i18n.conf

@@ -2045,12 +2045,18 @@ 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.
+Set <code>true</code> (default) to enable client authentication on this listener, the authentication
+process goes through the configured authentication chain.
+When set to <code>false</code> to allow any clients with or without authentication information such as username or password to log in.
+When set to <code>quick_deny_anonymous<code>, it behaves like when set to <code>true</code> but clients will be
+denied immediately without going through any authenticators if <code>username</code> is not provided. This is useful to fence off
+anonymous clients early.
 """
         zh: """
-配置 <code>true</code> (默认值)启用客户端进行身份认证。
-配置 <code>false</code> 时,将不对客户端做任何认证。
+配置 <code>true</code> (默认值)启用客户端进行身份认证,通过检查认配置的认认证器链来决定是否允许接入。
+配置 <code>false</code> 时,将不对客户端做任何认证,任何客户端,不论是不是携带用户名等认证信息,都可以接入。
+配置 <code>quick_deny_anonymous</code> 时,行为跟 <code>true</code> 类似,但是会对匿名
+客户直接拒绝,不做使用任何认证器对客户端进行身份检查。
 """
     }
     label: {

+ 14 - 3
apps/emqx/src/emqx_access_control.erl

@@ -38,11 +38,22 @@
     | {ok, map(), binary()}
     | {continue, map()}
     | {continue, binary(), map()}
-    | {error, term()}.
+    | {error, not_authorized}.
 authenticate(Credential) ->
-    case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of
+    %% pre-hook quick authentication or
+    %% if auth backend returning nothing but just 'ok'
+    %% it means it's not a superuser, or there is no way to tell.
+    NotSuperUser = #{is_superuser => false},
+    case emqx_authentication:pre_hook_authenticate(Credential) of
         ok ->
-            {ok, #{is_superuser => false}};
+            {ok, NotSuperUser};
+        continue ->
+            case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of
+                ok ->
+                    {ok, NotSuperUser};
+                Other ->
+                    Other
+            end;
         Other ->
             Other
     end.

+ 21 - 4
apps/emqx/src/emqx_authentication.erl

@@ -29,9 +29,13 @@
 -include_lib("stdlib/include/ms_transform.hrl").
 
 -define(CONF_ROOT, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
+-define(IS_UNDEFINED(X), (X =:= undefined orelse X =:= <<>>)).
 
 %% The authentication entrypoint.
--export([authenticate/2]).
+-export([
+    pre_hook_authenticate/1,
+    authenticate/2
+]).
 
 %% Authenticator manager process start/stop
 -export([
@@ -221,10 +225,23 @@ when
 %%------------------------------------------------------------------------------
 %% Authenticate
 %%------------------------------------------------------------------------------
-
-authenticate(#{enable_authn := false}, _AuthResult) ->
+-spec pre_hook_authenticate(emqx_types:clientinfo()) ->
+    ok | continue | {error, not_authorized}.
+pre_hook_authenticate(#{enable_authn := false}) ->
     inc_authenticate_metric('authentication.success.anonymous'),
-    ?TRACE_RESULT("authentication_result", ignore, enable_authn_false);
+    ?TRACE_RESULT("authentication_result", ok, enable_authn_false);
+pre_hook_authenticate(#{enable_authn := quick_deny_anonymous} = Credential) ->
+    case maps:get(username, Credential, undefined) of
+        U when ?IS_UNDEFINED(U) ->
+            ?TRACE_RESULT(
+                "authentication_result", {error, not_authorized}, enable_authn_false
+            );
+        _ ->
+            continue
+    end;
+pre_hook_authenticate(_) ->
+    continue.
+
 authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) ->
     case get_authenticators(Listener, global_chain(Protocol)) of
         {ok, ChainName, Authenticators} ->

+ 1 - 1
apps/emqx/src/emqx_schema.erl

@@ -1668,7 +1668,7 @@ base_listener(Bind) ->
             )},
         {"enable_authn",
             sc(
-                boolean(),
+                hoconsc:enum([true, false, quick_deny_anonymous]),
                 #{
                     desc => ?DESC(base_listener_enable_authn),
                     default => true

+ 38 - 1
apps/emqx/test/emqx_access_control_SUITE.erl

@@ -37,7 +37,8 @@ init_per_testcase(_, Config) ->
     Config.
 
 end_per_testcase(_, _Config) ->
-    ok = emqx_hooks:del('client.authorize', {?MODULE, authz_stub}).
+    ok = emqx_hooks:del('client.authorize', {?MODULE, authz_stub}),
+    ok = emqx_hooks:del('client.authenticate', {?MODULE, quick_deny_anonymous_authn}).
 
 t_authenticate(_) ->
     ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())).
@@ -60,6 +61,37 @@ t_delayed_authorize(_) ->
     ?assertEqual(deny, emqx_access_control:authorize(clientinfo(), Publish2, InvalidTopic)),
     ok.
 
+t_quick_deny_anonymous(_) ->
+    ok = emqx_hooks:put(
+        'client.authenticate',
+        {?MODULE, quick_deny_anonymous_authn, []},
+        ?HP_AUTHN
+    ),
+
+    RawClient0 = clientinfo(),
+    RawClient = RawClient0#{username => undefined},
+
+    %% No name, No authn
+    Client1 = RawClient#{enable_authn => false},
+    ?assertMatch({ok, _}, emqx_access_control:authenticate(Client1)),
+
+    %% No name, With quick_deny_anonymous
+    Client2 = RawClient#{enable_authn => quick_deny_anonymous},
+    ?assertMatch({error, _}, emqx_access_control:authenticate(Client2)),
+
+    %% Bad name, With quick_deny_anonymous
+    Client3 = RawClient#{enable_authn => quick_deny_anonymous, username => <<"badname">>},
+    ?assertMatch({error, _}, emqx_access_control:authenticate(Client3)),
+
+    %% Good name, With quick_deny_anonymous
+    Client4 = RawClient#{enable_authn => quick_deny_anonymous, username => <<"goodname">>},
+    ?assertMatch({ok, _}, emqx_access_control:authenticate(Client4)),
+
+    %% Name, With authn
+    Client5 = RawClient#{enable_authn => true, username => <<"badname">>},
+    ?assertMatch({error, _}, emqx_access_control:authenticate(Client5)),
+    ok.
+
 %%--------------------------------------------------------------------
 %% Helper functions
 %%--------------------------------------------------------------------
@@ -67,6 +99,11 @@ t_delayed_authorize(_) ->
 authz_stub(_Client, _PubSub, ValidTopic, _DefaultResult, ValidTopic) -> {stop, #{result => allow}};
 authz_stub(_Client, _PubSub, _Topic, _DefaultResult, _ValidTopic) -> {stop, #{result => deny}}.
 
+quick_deny_anonymous_authn(#{username := <<"badname">>}, _AuthResult) ->
+    {stop, {error, not_authorized}};
+quick_deny_anonymous_authn(_ClientInfo, _AuthResult) ->
+    {stop, {ok, #{is_superuser => false}}}.
+
 clientinfo() -> clientinfo(#{}).
 clientinfo(InitProps) ->
     maps:merge(

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

@@ -2319,5 +2319,4 @@ returncode_name(?SN_RC2_EXCEED_LIMITATION) -> rejected_exceed_limitation;
 returncode_name(?SN_RC2_REACHED_MAX_RETRY) -> reached_max_retry_times;
 returncode_name(_) -> accepted.
 
-name_to_returncode(not_authorized) -> ?SN_RC2_NOT_AUTHORIZE;
-name_to_returncode(_) -> ?SN_RC2_NOT_AUTHORIZE.
+name_to_returncode(not_authorized) -> ?SN_RC2_NOT_AUTHORIZE.

+ 2 - 0
changes/v5.0.11-en.md

@@ -23,6 +23,8 @@
 
 - Keep MQTT v5 User-Property pairs from bridge ingested MQTT messsages to bridge target [#9398](https://github.com/emqx/emqx/pull/9398).
 
+- Add a new config `quick_deny_anonymous` to allow quick deny of anonymous clients (without username) so the auth backend checks can be skipped [#8516](https://github.com/emqx/emqx/pull/8516).
+
 ## Bug fixes
 
 - Fix `ssl.existingName` option of  helm chart not working [#9307](https://github.com/emqx/emqx/issues/9307).

+ 2 - 0
changes/v5.0.11-zh.md

@@ -21,6 +21,8 @@
 
 - 为桥接收到的 MQTT v5 消息再转发时保留 User-Property 列表 [#9398](https://github.com/emqx/emqx/pull/9398)。
 
+- 添加了一个名为 `quick_deny_anonymous` 的新配置,用来在不调用认证链的情况下,快速的拒绝掉匿名用户,从而提高认证效率 [#8516](https://github.com/emqx/emqx/pull/8516)。
+
 ## 修复
 
 - 修复 helm chart 的 `ssl.existingName` 选项不起作用 [#9307](https://github.com/emqx/emqx/issues/9307)。