feat(emqx_auth_jwt): use JWT for ACL checks

This commit is contained in:
Ilya Averyanov 2022-04-11 23:00:31 +03:00
parent 36c1ecd9b7
commit 41746e8d7a
12 changed files with 304 additions and 82 deletions

View File

@ -15,6 +15,8 @@ File format:
### Enhancements
* Made possible for EMQX to boot from a Linux directory which has white spaces in its path.
* Add support for JWT authorization [#7596]
Now MQTT clients may be authorized with respect to a specific claim containing publish/subscribe topic whitelists.
## v4.3.14

View File

@ -46,6 +46,11 @@ auth.jwt.verify_claims = off
## - %u: username
## - %c: clientid
# auth.jwt.verify_claims.username = %u
## Name of the claim containg ACL rules
##
## Value: String
#auth.jwt.acl_claim_name = acl
```
Load the Plugin
@ -62,6 +67,33 @@ Example
mosquitto_pub -t 'pub' -m 'hello' -i test -u test -P eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYm9iIiwiYWdlIjoyOX0.bIV_ZQ8D5nQi0LT8AVkpM4Pd6wmlbpR9S8nOLJAsA8o
```
ACL
---
JWT may contain lists of topics allowed for subscribing/publishing (ACL rules):
Payload example:
```json
{
"sub": "emqx",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516239122,
"acl": {
"sub": [
"a/b",
"c/+",
"%u/%c"
],
"pub": [
"a/b",
"c/+",
"%u/%c"
]
}
}
```
Algorithms
----------

View File

@ -47,3 +47,8 @@ auth.jwt.verify_claims = off
## For example, to verify that the username in the JWT payload is the same
## as the client (MQTT protocol) username
#auth.jwt.verify_claims.username = %u
## Name of the claim containg ACL rules
##
## Value: String
#auth.jwt.acl_claim_name = acl

View File

@ -47,3 +47,12 @@
end, [], cuttlefish_variable:filter_by_prefix("auth.jwt.verify_claims", Conf))
end
end}.
{mapping, "auth.jwt.acl_claim_name", "emqx_auth_jwt.acl_claim_name", [
{default, "acl"},
{datatype, string}
]}.
{translation, "emqx_auth_jwt.acl_claim_name", fun(Conf) ->
list_to_binary(cuttlefish:conf_get("auth.jwt.acl_claim_name", Conf))
end}.

View File

@ -1,6 +1,6 @@
{application, emqx_auth_jwt,
[{description, "EMQ X Authentication with JWT"},
{vsn, "4.3.2"}, % strict semver, bump manually!
{vsn, "4.3.3"}, % strict semver, bump manually!
{modules, []},
{registered, [emqx_auth_jwt_sup]},
{applications, [kernel,stdlib,jose]},

View File

@ -1,15 +1,13 @@
%% -*-: erlang -*-
%% -*- mode: erlang -*-
%% Unless you know what you are doing, DO NOT edit manually!!
{VSN,
[
{<<"4\\.3\\.[0-1]">>, [
{load_module, emqx_auth_jwt_svr, brutal_purge, soft_purge, []}
]},
{<<".*">>, []}
],
[
{<<"4\\.3\\.[0-1]">>, [
{load_module, emqx_auth_jwt_svr, brutal_purge, soft_purge, []}
]},
{<<".*">>, []}
]
}.
[{"4.3.2",
[{restart_application,emqx_auth_jwt}]},
{<<"4\\.3\\.[0-1]">>,
[{restart_application,emqx_auth_jwt}]},
{<<".*">>,[]}],
[{"4.3.2",
[{restart_application,emqx_auth_jwt}]},
{<<"4\\.3\\.[0-1]">>,
[{restart_application,emqx_auth_jwt}]},
{<<".*">>,[]}]}.

View File

@ -22,7 +22,8 @@
-logger_header("[JWT]").
-export([ register_metrics/0
, check/3
, check_auth/3
, check_acl/5
, description/0
]).
@ -46,8 +47,7 @@ register_metrics() ->
%% Authentication callbacks
%%--------------------------------------------------------------------
check(ClientInfo, AuthResult, #{from := From,
checklists := Checklists}) ->
check_auth(ClientInfo, AuthResult, #{from := From, checklists := Checklists}) ->
case maps:find(From, ClientInfo) of
error ->
ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
@ -67,12 +67,38 @@ check(ClientInfo, AuthResult, #{from := From,
end
end.
check_acl(ClientInfo = #{jwt_claims := Claims},
PubSub,
Topic,
_NoMatchAction,
#{acl_claim_name := AclClaimName}) ->
Deadline = erlang:system_time(second),
case Claims of
#{AclClaimName := Acl, <<"exp">> := Exp}
when is_integer(Exp) andalso Exp >= Deadline ->
verify_acl(ClientInfo, Acl, PubSub, Topic);
_ -> ignore
end.
description() -> "Authentication with JWT".
%%------------------------------------------------------------------------------
%% Verify Claims
%%--------------------------------------------------------------------
verify_acl(ClientInfo, #{<<"sub">> := SubTopics}, subscribe, Topic) when is_list(SubTopics) ->
verify_acl(ClientInfo, SubTopics, Topic);
verify_acl(ClientInfo, #{<<"pub">> := PubTopics}, publish, Topic) when is_list(PubTopics) ->
verify_acl(ClientInfo, PubTopics, Topic);
verify_acl(_ClientInfo, _Acl, _PubSub, _Topic) -> {stop, deny}.
verify_acl(_ClientInfo, [], _Topic) -> {stop, deny};
verify_acl(ClientInfo, [AclTopic | AclTopics], Topic) ->
case match_topic(ClientInfo, AclTopic, Topic) of
true -> {stop, allow};
false -> verify_acl(ClientInfo, AclTopics, Topic)
end.
verify_claims(Checklists, Claims, ClientInfo) ->
case do_verify_claims(feedvar(Checklists, ClientInfo), Claims) of
{error, Reason} ->
@ -96,3 +122,9 @@ feedvar(Checklists, #{username := Username, clientid := ClientId}) ->
({K, <<"%c">>}) -> {K, ClientId};
({K, Expected}) -> {K, Expected}
end, Checklists).
match_topic(ClientInfo, AclTopic, Topic) ->
AclTopicWords = emqx_topic:words(AclTopic),
TopicWords = emqx_topic:words(Topic),
AclTopicRendered = emqx_access_rule:feed_var(ClientInfo, AclTopicWords),
emqx_topic:match(TopicWords, AclTopicRendered).

View File

@ -31,15 +31,20 @@
start(_Type, _Args) ->
{ok, Sup} = supervisor:start_link({local, ?MODULE}, ?MODULE, []),
{ok, Pid} = start_auth_server(jwks_svr_options()),
{ok, _} = start_auth_server(jwks_svr_options()),
ok = emqx_auth_jwt:register_metrics(),
AuthEnv = auth_env(),
_ = emqx:hook('client.authenticate', {emqx_auth_jwt, check_auth, [AuthEnv]}),
_ = emqx:hook('client.authenticate', {emqx_auth_jwt, check, [AuthEnv]}),
{ok, Sup, AuthEnv}.
AclEnv = acl_env(),
_ = emqx:hook('client.check_acl', {emqx_auth_jwt, check_acl, [AclEnv]}),
stop(AuthEnv) ->
emqx:unhook('client.authenticate', {emqx_auth_jwt, check, [AuthEnv]}).
{ok, Sup}.
stop(_State) ->
emqx:unhook('client.authenticate', {emqx_auth_jwt, check_auth}),
emqx:unhook('client.check_acl', {emqx_auth_jwt, check_acl}).
%%--------------------------------------------------------------------
%% Dummy supervisor
@ -68,6 +73,9 @@ auth_env() ->
, checklists => Checklists
}.
acl_env() ->
#{acl_claim_name => env(acl_claim_name, <<"acl">>)}.
jwks_svr_options() ->
[{K, V} || {K, V}
<- [{secret, env(secret, undefined)},

View File

@ -73,8 +73,7 @@ init([Options]) ->
ok = jose:json_module(jiffy),
_ = ets:new(?TAB, [set, protected, named_table]),
{Static, Remote} = do_init_jwks(Options),
true = ets:insert(?TAB, {static, Static}),
true = ets:insert(?TAB, {remote, Remote}),
true = ets:insert(?TAB, [{static, Static}, {remote, Remote}]),
Intv = proplists:get_value(interval, Options, ?INTERVAL),
{ok, reset_timer(
#state{
@ -181,7 +180,7 @@ do_verify(JwsCompacted) ->
end
catch
Class : Reason : Stk ->
?LOG(error, "Handle JWK crashed: ~p, ~p, stacktrace: ~p~n",
?LOG(error, "verify JWK crashed: ~p, ~p, stacktrace: ~p~n",
[Class, Reason, Stk]),
{error, invalid_signature}
end.
@ -219,11 +218,12 @@ check_claims(Claims) ->
do_check_claim([], Claims) ->
Claims;
do_check_claim([{K, F}|More], Claims) ->
case maps:take(K, Claims) of
error -> do_check_claim(More, Claims);
{V, NClaims} ->
case Claims of
#{K := V} ->
case F(V) of
true -> do_check_claim(More, NClaims);
true -> do_check_claim(More, Claims);
_ -> {false, K}
end
end;
_ ->
do_check_claim(More, Claims)
end.

View File

@ -19,29 +19,18 @@
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(APP, emqx_auth_jwt).
all() -> emqx_ct:all(?MODULE).
all() ->
[{group, emqx_auth_jwt}].
groups() ->
[{emqx_auth_jwt, [sequence], [ t_check_auth
, t_check_claims
, t_check_claims_clientid
, t_check_claims_username
, t_check_claims_kid_in_header
]}
].
init_per_suite(Config) ->
init_per_testcase(TestCase, Config) ->
?MODULE:TestCase(init, Config),
emqx_ct_helpers:start_apps([emqx_auth_jwt], fun set_special_configs/1),
Config.
end_per_suite(_Config) ->
end_per_testcase(_Case, _Config) ->
emqx_ct_helpers:stop_apps([emqx_auth_jwt]).
set_special_configs(emqx) ->
@ -78,7 +67,9 @@ sign(Payload, Alg, Key) ->
%% Testcases
%%------------------------------------------------------------------------------
t_check_auth(_) ->
t_check_auth(init, _Config) ->
application:unset_env(emqx_auth_jwt, verify_claims).
t_check_auth(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Jwt = sign([{clientid, <<"client1">>},
{username, <<"plain">>},
@ -102,10 +93,9 @@ t_check_auth(_) ->
?assertEqual({error, invalid_signature}, Result2),
?assertMatch({error, _}, emqx_access_control:authenticate(Plain#{password => <<"asd">>})).
t_check_claims(_) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]),
application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt),
t_check_claims(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_claims(_Config) ->
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
@ -120,9 +110,9 @@ t_check_claims(_) ->
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
?assertEqual({error, invalid_signature}, Result2).
t_check_claims_clientid(_) ->
application:set_env(emqx_auth_jwt, verify_claims, [{clientid, <<"%c">>}]),
application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt),
t_check_claims_clientid(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{clientid, <<"%c">>}]).
t_check_claims_clientid(_Config) ->
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
Jwt = sign([{clientid, <<"client23">>},
{username, <<"plain">>},
@ -136,10 +126,9 @@ t_check_claims_clientid(_) ->
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
?assertEqual({error, invalid_signature}, Result2).
t_check_claims_username(_) ->
application:set_env(emqx_auth_jwt, verify_claims, [{username, <<"%u">>}]),
application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt),
t_check_claims_username(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{username, <<"%u">>}]).
t_check_claims_username(_Config) ->
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
Jwt = sign([{client_id, <<"client23">>},
{username, <<"plain">>},
@ -153,8 +142,9 @@ t_check_claims_username(_) ->
ct:pal("Auth result for the invalid jwt: ~p~n", [Result3]),
?assertEqual({error, invalid_signature}, Result3).
t_check_claims_kid_in_header(_) ->
application:set_env(emqx_auth_jwt, verify_claims, []),
t_check_claims_kid_in_header(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, []).
t_check_claims_kid_in_header(_Config) ->
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
Jwt = sign([{clientid, <<"client23">>},
{username, <<"plain">>},
@ -164,3 +154,125 @@ t_check_claims_kid_in_header(_) ->
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
ct:pal("Auth result: ~p~n", [Result0]),
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0).
t_check_jwt_acl(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{acl, [{sub, [<<"a/b">>]},
{pub, [<<"c/d">>]}]},
{exp, os:system_time(seconds) + 10}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{password, Jwt}]),
{ok, _} = emqtt:connect(C),
?assertMatch(
{ok, #{}, [0]},
emqtt:subscribe(C, <<"a/b">>, 0)),
?assertMatch(
ok,
emqtt:publish(C, <<"c/d">>, <<"hi">>, 0)),
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"c/d">>, 0)),
ok = emqtt:publish(C, <<"a/b">>, <<"hi">>, 0),
receive
{publish, #{topic := <<"a/b">>}} ->
?assert(false, "Publish to `a/b` should not be allowed")
after 100 -> ok
end,
ok = emqtt:disconnect(C).
t_check_jwt_acl_no_recs(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl_no_recs(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{acl, []},
{exp, os:system_time(seconds) + 10}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{password, Jwt}]),
{ok, _} = emqtt:connect(C),
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"a/b">>, 0)),
ok = emqtt:disconnect(C).
t_check_jwt_acl_no_acl_claim(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl_no_acl_claim(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{exp, os:system_time(seconds) + 10}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{password, Jwt}]),
{ok, _} = emqtt:connect(C),
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"a/b">>, 0)),
ok = emqtt:disconnect(C).
t_check_jwt_acl_expire(init, _Config) ->
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]).
t_check_jwt_acl_expire(_Config) ->
Jwt = sign([{client_id, <<"client1">>},
{username, <<"plain">>},
{sub, value},
{acl, [{sub, [<<"a/b">>]}]},
{exp, os:system_time(seconds) + 1}],
<<"HS256">>,
<<"emqxsecret">>),
{ok, C} = emqtt:start_link(
[{clean_start, true},
{proto_ver, v5},
{client_id, <<"client1">>},
{password, Jwt}]),
{ok, _} = emqtt:connect(C),
?assertMatch(
{ok, #{}, [0]},
emqtt:subscribe(C, <<"a/b">>, 0)),
?assertMatch(
{ok, #{}, [0]},
emqtt:unsubscribe(C, <<"a/b">>)),
timer:sleep(2000),
?assertMatch(
{ok, #{}, [?RC_NOT_AUTHORIZED]},
emqtt:subscribe(C, <<"a/b">>, 0)),
ok = emqtt:disconnect(C).

View File

@ -1,9 +1,13 @@
%% -*- mode: erlang -*-
%% Unless you know what you are doing, DO NOT edit manually!!
{VSN,
[{"4.3.15",[{load_module,emqx_frame,brutal_purge,soft_purge,[]}]},
[{"4.3.15",
[{load_module,emqx_frame,brutal_purge,soft_purge,[]},
{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}]},
{"4.3.14",
[{load_module,emqx,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_sys,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
@ -11,7 +15,8 @@
{load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]}]},
{"4.3.13",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
@ -28,7 +33,8 @@
{load_module,emqx_misc,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]}]},
{"4.3.12",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_frame,brutal_purge,soft_purge,[]},
@ -53,7 +59,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.11",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -80,7 +87,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.10",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -107,7 +115,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.9",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -138,7 +147,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.8",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -169,7 +179,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.7",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -201,7 +212,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.6",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -444,9 +456,13 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{<<".*">>,[]}],
[{"4.3.15",[{load_module,emqx_frame,brutal_purge,soft_purge,[]}]},
[{"4.3.15",
[{load_module,emqx_frame,brutal_purge,soft_purge,[]},
{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}]},
{"4.3.14",
[{load_module,emqx,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_sys,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
@ -454,7 +470,8 @@
{load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]}]},
{"4.3.13",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
@ -471,7 +488,8 @@
{load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]}]},
{"4.3.12",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_frame,brutal_purge,soft_purge,[]},
@ -495,7 +513,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.11",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -521,7 +540,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.10",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -547,7 +567,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.9",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -577,7 +598,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.8",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -607,7 +629,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.7",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},
@ -638,7 +661,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.6",
[{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
[{load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_shared_sub,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_pmon,brutal_purge,soft_purge,[]},

View File

@ -19,6 +19,7 @@
%% APIs
-export([ match/3
, compile/1
, feed_var/2
]).
-export_type([rule/0]).
@ -158,4 +159,3 @@ feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) ->
feed_var(ClientInfo, Words, [Username|Acc]);
feed_var(ClientInfo, [W|Words], Acc) ->
feed_var(ClientInfo, Words, [W|Acc]).