477 lines
13 KiB
Erlang
477 lines
13 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2022-2023 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_authz_jwt_SUITE).
|
|
|
|
-compile(nowarn_export_all).
|
|
-compile(export_all).
|
|
|
|
-include_lib("emqx/include/emqx_placeholder.hrl").
|
|
-include_lib("emqx_auth/include/emqx_authn.hrl").
|
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
|
-include_lib("emqx/include/emqx_access_control.hrl").
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include_lib("common_test/include/ct.hrl").
|
|
|
|
-define(SECRET, <<"some_secret">>).
|
|
-define(AUTHN_PATH, [authentication]).
|
|
|
|
all() ->
|
|
emqx_common_test_helpers:all(?MODULE).
|
|
|
|
groups() ->
|
|
[].
|
|
|
|
init_per_suite(Config) ->
|
|
Apps = emqx_cth_suite:start(
|
|
[
|
|
emqx,
|
|
{emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"},
|
|
emqx_auth,
|
|
emqx_auth_jwt
|
|
],
|
|
#{work_dir => ?config(priv_dir, Config)}
|
|
),
|
|
[{suite_apps, Apps} | Config].
|
|
|
|
end_per_suite(Config) ->
|
|
ok = emqx_authz_test_lib:restore_authorizers(),
|
|
emqx_cth_suite:stop(?config(suite_apps, Config)).
|
|
|
|
init_per_testcase(_TestCase, Config) ->
|
|
emqx_authn_test_lib:delete_authenticators(
|
|
?AUTHN_PATH,
|
|
?GLOBAL
|
|
),
|
|
AuthConfig = authn_config(),
|
|
{ok, _} = emqx:update_config(
|
|
?AUTHN_PATH,
|
|
{create_authenticator, ?GLOBAL, AuthConfig}
|
|
),
|
|
|
|
ok = emqx_authz_test_lib:reset_authorizers(),
|
|
Config.
|
|
|
|
end_per_testcase(_TestCase, _Config) ->
|
|
emqx_authn_test_lib:delete_authenticators(
|
|
?AUTHN_PATH,
|
|
?GLOBAL
|
|
),
|
|
ok = emqx_authz_test_lib:restore_authorizers().
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% Tests
|
|
%%------------------------------------------------------------------------------
|
|
|
|
t_topic_rules(_Config) ->
|
|
JWT = #{
|
|
<<"exp">> => erlang:system_time(second) + 60,
|
|
<<"acl">> => #{
|
|
<<"pub">> => [
|
|
<<"eq testpub1/${username}">>,
|
|
<<"testpub2/${clientid}">>,
|
|
<<"testpub3/#">>
|
|
],
|
|
<<"sub">> => [
|
|
<<"eq testsub1/${username}">>,
|
|
<<"testsub2/${clientid}">>,
|
|
<<"testsub3/#">>
|
|
],
|
|
<<"all">> => [
|
|
<<"eq testall1/${username}">>,
|
|
<<"testall2/${clientid}">>,
|
|
<<"testall3/#">>
|
|
]
|
|
},
|
|
<<"username">> => <<"username">>
|
|
},
|
|
test_topic_rules(JWT).
|
|
|
|
t_topic_rules_v2(_Config) ->
|
|
JWT = #{
|
|
<<"exp">> => erlang:system_time(second) + 60,
|
|
<<"acl">> => [
|
|
#{
|
|
<<"permission">> => <<"allow">>,
|
|
<<"action">> => <<"pub">>,
|
|
<<"topics">> => [
|
|
<<"eq testpub1/${username}">>,
|
|
<<"testpub2/${clientid}">>,
|
|
<<"testpub3/#">>
|
|
]
|
|
},
|
|
#{
|
|
<<"permission">> => <<"allow">>,
|
|
<<"action">> => <<"sub">>,
|
|
<<"topics">> =>
|
|
[
|
|
<<"eq testsub1/${username}">>,
|
|
<<"testsub2/${clientid}">>,
|
|
<<"testsub3/#">>
|
|
]
|
|
},
|
|
#{
|
|
<<"permission">> => <<"allow">>,
|
|
<<"action">> => <<"all">>,
|
|
<<"topics">> => [
|
|
<<"eq testall1/${username}">>,
|
|
<<"testall2/${clientid}">>,
|
|
<<"testall3/#">>
|
|
]
|
|
}
|
|
],
|
|
<<"username">> => <<"username">>
|
|
},
|
|
test_topic_rules(JWT).
|
|
|
|
test_topic_rules(JWTInput) ->
|
|
JWT = generate_jws(JWTInput),
|
|
|
|
{ok, C} = emqtt:start_link(
|
|
[
|
|
{clean_start, true},
|
|
{proto_ver, v5},
|
|
{clientid, <<"clientid">>},
|
|
{username, <<"username">>},
|
|
{password, JWT}
|
|
]
|
|
),
|
|
{ok, _} = emqtt:connect(C),
|
|
|
|
Cases = [
|
|
{deny, <<"testpub1/username">>},
|
|
{deny, <<"testpub2/clientid">>},
|
|
{deny, <<"testpub3/foobar">>},
|
|
|
|
{deny, <<"testsub1/username">>},
|
|
{allow, <<"testsub1/${username}">>},
|
|
{allow, <<"testsub2/clientid">>},
|
|
{allow, <<"testsub3/foobar">>},
|
|
{allow, <<"testsub3/+/foobar">>},
|
|
{allow, <<"testsub3/#">>},
|
|
|
|
{deny, <<"testsub2/username">>},
|
|
{deny, <<"testsub1/clientid">>},
|
|
{deny, <<"testsub4/foobar">>},
|
|
|
|
{deny, <<"testall1/username">>},
|
|
{allow, <<"testall1/${username}">>},
|
|
{allow, <<"testall2/clientid">>},
|
|
{allow, <<"testall3/foobar">>},
|
|
{allow, <<"testall3/+/foobar">>},
|
|
{allow, <<"testall3/#">>},
|
|
|
|
{deny, <<"testall2/username">>},
|
|
{deny, <<"testall1/clientid">>},
|
|
{deny, <<"testall4/foobar">>}
|
|
],
|
|
|
|
lists:foreach(
|
|
fun
|
|
({allow, Topic}) ->
|
|
?assertMatch(
|
|
{ok, #{}, [0]},
|
|
emqtt:subscribe(C, Topic, 0)
|
|
);
|
|
({deny, Topic}) ->
|
|
?assertMatch(
|
|
{ok, #{}, [?RC_NOT_AUTHORIZED]},
|
|
emqtt:subscribe(C, Topic, 0)
|
|
)
|
|
end,
|
|
Cases
|
|
),
|
|
|
|
ok = emqtt:disconnect(C).
|
|
|
|
t_check_pub(_Config) ->
|
|
Payload = #{
|
|
<<"username">> => <<"username">>,
|
|
<<"acl">> => #{<<"sub">> => [<<"a/b">>]},
|
|
<<"exp">> => erlang:system_time(second) + 10
|
|
},
|
|
|
|
JWT = generate_jws(Payload),
|
|
|
|
{ok, C} = emqtt:start_link(
|
|
[
|
|
{clean_start, true},
|
|
{proto_ver, v5},
|
|
{clientid, <<"clientid">>},
|
|
{username, <<"username">>},
|
|
{password, JWT}
|
|
]
|
|
),
|
|
{ok, _} = emqtt:connect(C),
|
|
|
|
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_no_recs(_Config) ->
|
|
Payload = #{
|
|
<<"username">> => <<"username">>,
|
|
<<"acl">> => #{},
|
|
<<"exp">> => erlang:system_time(second) + 10
|
|
},
|
|
|
|
JWT = generate_jws(Payload),
|
|
|
|
{ok, C} = emqtt:start_link(
|
|
[
|
|
{clean_start, true},
|
|
{proto_ver, v5},
|
|
{clientid, <<"clientid">>},
|
|
{username, <<"username">>},
|
|
{password, JWT}
|
|
]
|
|
),
|
|
{ok, _} = emqtt:connect(C),
|
|
|
|
?assertMatch(
|
|
{ok, #{}, [?RC_NOT_AUTHORIZED]},
|
|
emqtt:subscribe(C, <<"a/b">>, 0)
|
|
),
|
|
|
|
ok = emqtt:disconnect(C).
|
|
|
|
t_check_no_acl_claim(_Config) ->
|
|
Payload = #{
|
|
<<"username">> => <<"username">>,
|
|
<<"exp">> => erlang:system_time(second) + 10
|
|
},
|
|
|
|
JWT = generate_jws(Payload),
|
|
|
|
{ok, C} = emqtt:start_link(
|
|
[
|
|
{clean_start, true},
|
|
{proto_ver, v5},
|
|
{clientid, <<"clientid">>},
|
|
{username, <<"username">>},
|
|
{password, JWT}
|
|
]
|
|
),
|
|
{ok, _} = emqtt:connect(C),
|
|
|
|
?assertMatch(
|
|
{ok, #{}, [?RC_NOT_AUTHORIZED]},
|
|
emqtt:subscribe(C, <<"a/b">>, 0)
|
|
),
|
|
|
|
ok = emqtt:disconnect(C).
|
|
|
|
t_check_str_exp(_Config) ->
|
|
Payload = #{
|
|
<<"username">> => <<"username">>,
|
|
<<"exp">> => integer_to_binary(erlang:system_time(second) + 10),
|
|
<<"acl">> => #{<<"sub">> => [<<"a/b">>]}
|
|
},
|
|
|
|
JWT = generate_jws(Payload),
|
|
|
|
{ok, C} = emqtt:start_link(
|
|
[
|
|
{clean_start, true},
|
|
{proto_ver, v5},
|
|
{clientid, <<"clientid">>},
|
|
{username, <<"username">>},
|
|
{password, JWT}
|
|
]
|
|
),
|
|
{ok, _} = emqtt:connect(C),
|
|
|
|
?assertMatch(
|
|
{ok, #{}, [0]},
|
|
emqtt:subscribe(C, <<"a/b">>, 0)
|
|
),
|
|
|
|
ok = emqtt:disconnect(C).
|
|
|
|
t_check_expire(_Config) ->
|
|
Payload = #{
|
|
<<"username">> => <<"username">>,
|
|
<<"acl">> => #{<<"sub">> => [<<"a/b">>]},
|
|
<<"exp">> => erlang:system_time(second) + 5
|
|
},
|
|
|
|
JWT = generate_jws(Payload),
|
|
|
|
{ok, C} = emqtt:start_link(
|
|
[
|
|
{clean_start, true},
|
|
{proto_ver, v5},
|
|
{clientid, <<"clientid">>},
|
|
{username, <<"username">>},
|
|
{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(6000),
|
|
|
|
?assertMatch(
|
|
{ok, #{}, [?RC_NOT_AUTHORIZED]},
|
|
emqtt:subscribe(C, <<"a/b">>, 0)
|
|
),
|
|
|
|
ok = emqtt:disconnect(C).
|
|
|
|
t_check_no_expire(_Config) ->
|
|
Payload = #{
|
|
<<"username">> => <<"username">>,
|
|
<<"acl">> => #{<<"sub">> => [<<"a/b">>]}
|
|
},
|
|
|
|
JWT = generate_jws(Payload),
|
|
|
|
{ok, C} = emqtt:start_link(
|
|
[
|
|
{clean_start, true},
|
|
{proto_ver, v5},
|
|
{clientid, <<"clientid">>},
|
|
{username, <<"username">>},
|
|
{password, JWT}
|
|
]
|
|
),
|
|
{ok, _} = emqtt:connect(C),
|
|
?assertMatch(
|
|
{ok, #{}, [0]},
|
|
emqtt:subscribe(C, <<"a/b">>, 0)
|
|
),
|
|
|
|
?assertMatch(
|
|
{ok, #{}, [0]},
|
|
emqtt:unsubscribe(C, <<"a/b">>)
|
|
),
|
|
|
|
ok = emqtt:disconnect(C).
|
|
|
|
t_check_undefined_expire(_Config) ->
|
|
Acl = #{expire => undefined, rules => #{<<"sub">> => [<<"a/b">>]}},
|
|
Client = #{acl => Acl},
|
|
|
|
?assertMatch(
|
|
{matched, allow},
|
|
emqx_authz_client_info:authorize(Client, ?AUTHZ_SUBSCRIBE, <<"a/b">>, undefined)
|
|
),
|
|
|
|
?assertMatch(
|
|
{matched, deny},
|
|
emqx_authz_client_info:authorize(Client, ?AUTHZ_SUBSCRIBE, <<"a/bar">>, undefined)
|
|
).
|
|
|
|
t_invalid_rule(_Config) ->
|
|
emqx_logger:set_log_level(debug),
|
|
MakeJWT = fun(Acl) ->
|
|
generate_jws(#{
|
|
<<"exp">> => erlang:system_time(second) + 60,
|
|
<<"username">> => <<"username">>,
|
|
<<"acl">> => Acl
|
|
})
|
|
end,
|
|
InvalidAclList =
|
|
[
|
|
%% missing action
|
|
[#{<<"permission">> => <<"invalid">>}],
|
|
%% missing topic or topics
|
|
[#{<<"permission">> => <<"allow">>, <<"action">> => <<"pub">>}],
|
|
%% invlaid permission, must be allow | deny
|
|
[
|
|
#{
|
|
<<"permission">> => <<"invalid">>,
|
|
<<"action">> => <<"pub">>,
|
|
<<"topic">> => <<"t">>
|
|
}
|
|
],
|
|
%% invalid action, must be pub | sub | all
|
|
[
|
|
#{
|
|
<<"permission">> => <<"allow">>,
|
|
<<"action">> => <<"invalid">>,
|
|
<<"topic">> => <<"t">>
|
|
}
|
|
],
|
|
%% invalid qos
|
|
[
|
|
#{
|
|
<<"permission">> => <<"allow">>,
|
|
<<"action">> => <<"pub">>,
|
|
<<"topics">> => [<<"t">>],
|
|
<<"qos">> => 3
|
|
}
|
|
]
|
|
],
|
|
lists:foreach(
|
|
fun(InvalidAcl) ->
|
|
{ok, C} = emqtt:start_link(
|
|
[
|
|
{clean_start, true},
|
|
{proto_ver, v5},
|
|
{clientid, <<"clientid">>},
|
|
{username, <<"username">>},
|
|
{password, MakeJWT(InvalidAcl)}
|
|
]
|
|
),
|
|
unlink(C),
|
|
?assertMatch({error, {bad_username_or_password, _}}, emqtt:connect(C))
|
|
end,
|
|
InvalidAclList
|
|
).
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% Helpers
|
|
%%------------------------------------------------------------------------------
|
|
|
|
authn_config() ->
|
|
#{
|
|
<<"mechanism">> => <<"jwt">>,
|
|
<<"use_jwks">> => <<"false">>,
|
|
<<"algorithm">> => <<"hmac-based">>,
|
|
<<"secret">> => ?SECRET,
|
|
<<"secret_base64_encoded">> => <<"false">>,
|
|
<<"acl_claim_name">> => <<"acl">>,
|
|
<<"verify_claims">> => #{
|
|
<<"username">> => ?PH_USERNAME
|
|
}
|
|
}.
|
|
|
|
generate_jws(Payload) ->
|
|
JWK = jose_jwk:from_oct(?SECRET),
|
|
Header = #{
|
|
<<"alg">> => <<"HS256">>,
|
|
<<"typ">> => <<"JWT">>
|
|
},
|
|
Signed = jose_jwt:sign(JWK, Header, Payload),
|
|
{_, JWS} = jose_jws:compact(Signed),
|
|
JWS.
|