feat(authn): add enable_authn flag for listeners

This commit is contained in:
Ilya Averyanov 2022-06-15 16:50:05 +03:00
parent 3951d6840f
commit e381e3698f
17 changed files with 219 additions and 67 deletions

View File

@ -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 { mqtt_listener_access_rules {
desc { desc {
en: """ en: """

View File

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

View File

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

View File

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

View File

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

View File

@ -38,8 +38,6 @@
]). ]).
-export([ -export([
change_emqx_opts/1,
change_emqx_opts/2,
client_ssl/0, client_ssl/0,
client_ssl/1, client_ssl/1,
client_ssl_twoway/0, 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), {Pid, Mref} = erlang:spawn_monitor(fun() -> wait_loop(F, catch_call(F)) end),
wait_for_down(Fn, Ln, Timeout, Pid, Mref, false). 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() ->
flush([]). flush([]).

View File

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

View File

@ -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 { gateway_common_listener_mountpoint {
desc { desc {
en: """When publishing or subscribing, prefix all topics with a mountpoint string. en: """When publishing or subscribing, prefix all topics with a mountpoint string.

View File

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

View File

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

View File

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

View File

@ -157,7 +157,12 @@ init(
undefined -> undefined; undefined -> undefined;
{GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName) {GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
end, 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{ Channel = #channel{
ctx = Ctx, ctx = Ctx,
gcli = #{channel => GRpcChann, pool_name => PoolName}, gcli = #{channel => GRpcChann, pool_name => PoolName},

View File

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

View File

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

View File

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

View File

@ -109,6 +109,12 @@ t_case_coap(_) ->
Prefix ++ Prefix ++
"/connection?clientid=client1&username=bad&password=bad", "/connection?clientid=client1&username=bad&password=bad",
Login(LeftUrl, ?checkMatch({error, bad_request, _Data})), 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. ok.
-record(coap_content, {content_format, payload = <<>>}). -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", NoInfoUrl = "coap://127.0.0.1:~b/rd?ep=~ts&lt=345&lwm2m=1",
Login(NoInfoUrl, MakeCheker(ack, {error, bad_request})), 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. ok.
-define(SN_CONNACK, 16#05). -define(SN_CONNACK, 16#05).
@ -182,6 +193,9 @@ t_case_mqttsn(_) ->
end, end,
Login(<<"badadmin">>, <<"badpassowrd">>, <<3, ?SN_CONNACK, 16#80>>), Login(<<"badadmin">>, <<"badpassowrd">>, <<3, ?SN_CONNACK, 16#80>>),
Login(<<"admin">>, <<"public">>, <<3, ?SN_CONNACK, 0>>), Login(<<"admin">>, <<"public">>, <<3, ?SN_CONNACK, 0>>),
disable_authn(mqttsn, udp, default),
Login(<<"badadmin">>, <<"badpassowrd">>, <<3, ?SN_CONNACK, 0>>),
ok. ok.
t_case_stomp(_) -> t_case_stomp(_) ->
@ -220,6 +234,15 @@ t_case_stomp(_) ->
?assertEqual(<<"Login Failed: not_authorized">>, Mod:get_field(body, Frame)) ?assertEqual(<<"Login Failed: not_authorized">>, Mod:get_field(body, Frame))
end), end),
disable_authn(stomp, tcp, default),
Login(
<<"bad">>,
<<"bad">>,
?FUNCTOR(
Frame,
?assertEqual(<<"CONNECTED">>, Mod:get_field(command, Frame))
)
),
ok. ok.
t_case_exproto(_) -> t_case_exproto(_) ->
@ -249,5 +272,18 @@ t_case_exproto(_) ->
end, end,
Login(<<"admin">>, <<"public">>, SvrMod:frame_connack(0)), Login(<<"admin">>, <<"public">>, SvrMod:frame_connack(0)),
Login(<<"bad">>, <<"bad">>, SvrMod:frame_connack(1)), Login(<<"bad">>, <<"bad">>, SvrMod:frame_connack(1)),
disable_authn(exproto, tcp, default),
Login(<<"bad">>, <<"bad">>, SvrMod:frame_connack(0)),
SvrMod:stop(Svrs), SvrMod:stop(Svrs),
ok. 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
}).

View File

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