From 2bc8b004199a7a5fd2942fa70c331468728ef98f Mon Sep 17 00:00:00 2001 From: firest Date: Thu, 24 Nov 2022 11:16:25 +0800 Subject: [PATCH] feat(authn): support quick deny anonymous --- apps/emqx/i18n/emqx_schema_i18n.conf | 14 +++++-- apps/emqx/src/emqx_access_control.erl | 17 ++++++-- apps/emqx/src/emqx_authentication.erl | 25 ++++++++++-- apps/emqx/src/emqx_schema.erl | 2 +- apps/emqx/test/emqx_access_control_SUITE.erl | 39 ++++++++++++++++++- .../src/mqttsn/emqx_sn_channel.erl | 3 +- changes/v5.0.11-en.md | 2 + changes/v5.0.11-zh.md | 2 + 8 files changed, 89 insertions(+), 15 deletions(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 714a08704..e7c4890f7 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -2045,12 +2045,18 @@ Type of the rate limit. base_listener_enable_authn { desc { en: """ -Set true (default) to enable client authentication on this listener. -When set to false clients will be allowed to connect without authentication. +Set true (default) to enable client authentication on this listener, the authentication +process goes through the configured authentication chain. +When set to false to allow any clients with or without authentication information such as username or password to log in. +When set to quick_deny_anonymous, it behaves like when set to true but clients will be +denied immediately without going through any authenticators if username is not provided. This is useful to fence off +anonymous clients early. """ zh: """ -配置 true (默认值)启用客户端进行身份认证。 -配置 false 时,将不对客户端做任何认证。 +配置 true (默认值)启用客户端进行身份认证,通过检查认配置的认认证器链来决定是否允许接入。 +配置 false 时,将不对客户端做任何认证,任何客户端,不论是不是携带用户名等认证信息,都可以接入。 +配置 quick_deny_anonymous 时,行为跟 true 类似,但是会对匿名 +客户直接拒绝,不做使用任何认证器对客户端进行身份检查。 """ } label: { diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index d99699a9a..30d56f257 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/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. diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl index 964a97dfb..749f5bfd7 100644 --- a/apps/emqx/src/emqx_authentication.erl +++ b/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} -> diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 1024cef48..eb5238cfe 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/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 diff --git a/apps/emqx/test/emqx_access_control_SUITE.erl b/apps/emqx/test/emqx_access_control_SUITE.erl index 7b6b4f463..c079ac125 100644 --- a/apps/emqx/test/emqx_access_control_SUITE.erl +++ b/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( diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index b5e051193..df3b4018e 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/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. diff --git a/changes/v5.0.11-en.md b/changes/v5.0.11-en.md index e53c5785e..6ad40c7fe 100644 --- a/changes/v5.0.11-en.md +++ b/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). diff --git a/changes/v5.0.11-zh.md b/changes/v5.0.11-zh.md index 3ea516dad..b5fe7c4bd 100644 --- a/changes/v5.0.11-zh.md +++ b/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)。