feat(authn): support quick deny anonymous

This commit is contained in:
firest 2022-11-24 11:16:25 +08:00 committed by Zaiming (Stone) Shi
parent cded5fc6cf
commit 2bc8b00419
8 changed files with 89 additions and 15 deletions

View File

@ -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: {

View File

@ -38,11 +38,22 @@
| {ok, map(), binary()}
| {continue, map()}
| {continue, binary(), map()}
| {error, term()}.
| {error, not_authorized}.
authenticate(Credential) ->
%% 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, NotSuperUser};
continue ->
case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of
ok ->
{ok, #{is_superuser => false}};
{ok, NotSuperUser};
Other ->
Other
end;
Other ->
Other
end.

View File

@ -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} ->

View File

@ -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

View File

@ -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(

View File

@ -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.

View File

@ -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).

View File

@ -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)。