| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626 |
- %%--------------------------------------------------------------------
- %% Copyright (c) 2020-2024 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_http_SUITE).
- -compile(nowarn_export_all).
- -compile(export_all).
- -include_lib("emqx_auth/include/emqx_authz.hrl").
- -include_lib("eunit/include/eunit.hrl").
- -include_lib("common_test/include/ct.hrl").
- -include_lib("emqx/include/emqx_placeholder.hrl").
- -include_lib("snabbkaffe/include/snabbkaffe.hrl").
- -define(HTTP_PORT, 33333).
- -define(HTTP_PATH, "/authz/[...]").
- -define(AUTHZ_HTTP_RESP(Result, Req),
- cowboy_req:reply(
- 200,
- #{<<"content-type">> => <<"application/json">>},
- "{\"result\": \"" ++ atom_to_list(Result) ++ "\"}",
- Req
- )
- ).
- all() ->
- emqx_common_test_helpers:all(?MODULE).
- init_per_suite(Config) ->
- Apps = emqx_cth_suite:start(
- [
- emqx,
- {emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"},
- emqx_auth,
- emqx_auth_http
- ],
- #{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(_Case, Config) ->
- ok = emqx_authz_test_lib:reset_authorizers(),
- {ok, _} = emqx_authz_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH),
- Config.
- end_per_testcase(_Case, _Config) ->
- _ = emqx_authz:set_feature_available(rich_actions, true),
- try
- ok = emqx_authz_http_test_server:stop()
- catch
- exit:noproc ->
- ok
- end,
- snabbkaffe:stop(),
- ok.
- %%------------------------------------------------------------------------------
- %% Tests
- %%------------------------------------------------------------------------------
- t_response_handling(_Config) ->
- ClientInfo = #{
- clientid => <<"clientid">>,
- username => <<"username">>,
- peerhost => {127, 0, 0, 1},
- zone => default,
- listener => {tcp, default}
- },
- %% OK, get, body & headers
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- {ok, ?AUTHZ_HTTP_RESP(allow, Req0), State}
- end,
- #{}
- ),
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ),
- %% Not OK, get, no body
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- Req = cowboy_req:reply(200, Req0),
- {ok, Req, State}
- end,
- #{}
- ),
- deny = emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>),
- %% OK, get, 204
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- Req = cowboy_req:reply(204, Req0),
- {ok, Req, State}
- end,
- #{}
- ),
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ),
- %% Not OK, get, 400
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- Req = cowboy_req:reply(400, Req0),
- {ok, Req, State}
- end,
- #{}
- ),
- ?assertEqual(
- deny,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ),
- %% Not OK, get, 400 + body & headers
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- Req = cowboy_req:reply(
- 400,
- #{<<"content-type">> => <<"text/plain">>},
- "Response body",
- Req0
- ),
- {ok, Req, State}
- end,
- #{}
- ),
- ?assertEqual(
- deny,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ),
- %% the server cannot be reached; should skip to the next
- %% authorizer in the chain.
- ok = emqx_authz_http_test_server:stop(),
- ?check_trace(
- ?assertEqual(
- deny,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ),
- fun(Trace) ->
- ?assertMatch(
- [
- #{
- ?snk_kind := authz_http_request_failure,
- error := {recoverable_error, econnrefused}
- }
- ],
- ?of_kind(authz_http_request_failure, Trace)
- ),
- ?assert(
- ?strict_causality(
- #{?snk_kind := authz_http_request_failure},
- #{?snk_kind := authz_non_superuser, result := nomatch},
- Trace
- )
- ),
- ok
- end
- ),
- ok.
- t_query_params(_Config) ->
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- #{
- username := <<"user name">>,
- clientid := <<"client id">>,
- peerhost := <<"127.0.0.1">>,
- proto_name := <<"MQTT">>,
- mountpoint := <<"MOUNTPOINT">>,
- topic := <<"t/1">>,
- action := <<"publish">>,
- qos := <<"1">>,
- retain := <<"false">>
- } = cowboy_req:match_qs(
- [
- username,
- clientid,
- peerhost,
- proto_name,
- mountpoint,
- topic,
- action,
- qos,
- retain
- ],
- Req0
- ),
- {ok, ?AUTHZ_HTTP_RESP(allow, Req0), State}
- end,
- #{
- <<"url">> => <<
- "http://127.0.0.1:33333/authz/users/?"
- "username=${username}&"
- "clientid=${clientid}&"
- "peerhost=${peerhost}&"
- "proto_name=${proto_name}&"
- "mountpoint=${mountpoint}&"
- "topic=${topic}&"
- "action=${action}&"
- "qos=${qos}&"
- "retain=${retain}"
- >>
- }
- ),
- ClientInfo = #{
- clientid => <<"client id">>,
- username => <<"user name">>,
- peerhost => {127, 0, 0, 1},
- protocol => <<"MQTT">>,
- mountpoint => <<"MOUNTPOINT">>,
- zone => default,
- listener => {tcp, default}
- },
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t/1">>)
- ).
- t_path(_Config) ->
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- ?assertEqual(
- <<
- "/authz/use%20rs/"
- "user%20name/"
- "client%20id/"
- "127.0.0.1/"
- "MQTT/"
- "MOUNTPOINT/"
- "t%2F1/"
- "publish/"
- "1/"
- "false"
- >>,
- cowboy_req:path(Req0)
- ),
- {ok, ?AUTHZ_HTTP_RESP(allow, Req0), State}
- end,
- #{
- <<"url">> => <<
- "http://127.0.0.1:33333/authz/use%20rs/"
- "${username}/"
- "${clientid}/"
- "${peerhost}/"
- "${proto_name}/"
- "${mountpoint}/"
- "${topic}/"
- "${action}/"
- "${qos}/"
- "${retain}"
- >>
- }
- ),
- ClientInfo = #{
- clientid => <<"client id">>,
- username => <<"user name">>,
- peerhost => {127, 0, 0, 1},
- protocol => <<"MQTT">>,
- mountpoint => <<"MOUNTPOINT">>,
- zone => default,
- listener => {tcp, default}
- },
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t/1">>)
- ).
- t_json_body(_Config) ->
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- ?assertEqual(
- <<"/authz/users/">>,
- cowboy_req:path(Req0)
- ),
- {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
- ?assertMatch(
- #{
- <<"username">> := <<"user name">>,
- <<"CLIENT">> := <<"client id">>,
- <<"peerhost">> := <<"127.0.0.1">>,
- <<"proto_name">> := <<"MQTT">>,
- <<"mountpoint">> := <<"MOUNTPOINT">>,
- <<"topic">> := <<"t">>,
- <<"action">> := <<"publish">>,
- <<"qos">> := <<"1">>,
- <<"retain">> := <<"false">>
- },
- emqx_utils_json:decode(RawBody, [return_maps])
- ),
- {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
- end,
- #{
- <<"method">> => <<"post">>,
- <<"body">> => #{
- <<"username">> => <<"${username}">>,
- <<"CLIENT">> => <<"${clientid}">>,
- <<"peerhost">> => <<"${peerhost}">>,
- <<"proto_name">> => <<"${proto_name}">>,
- <<"mountpoint">> => <<"${mountpoint}">>,
- <<"topic">> => <<"${topic}">>,
- <<"action">> => <<"${action}">>,
- <<"qos">> => <<"${qos}">>,
- <<"retain">> => <<"${retain}">>
- }
- }
- ),
- ClientInfo = #{
- clientid => <<"client id">>,
- username => <<"user name">>,
- peerhost => {127, 0, 0, 1},
- protocol => <<"MQTT">>,
- mountpoint => <<"MOUNTPOINT">>,
- zone => default,
- listener => {tcp, default}
- },
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t">>)
- ).
- t_no_rich_actions(_Config) ->
- _ = emqx_authz:set_feature_available(rich_actions, false),
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- ?assertEqual(
- <<"/authz/users/">>,
- cowboy_req:path(Req0)
- ),
- {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
- %% No interpolation if rich_actions is disabled
- ?assertMatch(
- #{
- <<"qos">> := <<"${qos}">>,
- <<"retain">> := <<"${retain}">>
- },
- emqx_utils_json:decode(RawBody, [return_maps])
- ),
- {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
- end,
- #{
- <<"method">> => <<"post">>,
- <<"body">> => #{
- <<"qos">> => <<"${qos}">>,
- <<"retain">> => <<"${retain}">>
- }
- }
- ),
- ClientInfo = emqx_authz_test_lib:base_client_info(),
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t">>)
- ).
- t_placeholder_and_body(_Config) ->
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- ?assertEqual(
- <<"/authz/users/">>,
- cowboy_req:path(Req0)
- ),
- {ok, [{PostVars, true}], Req1} = cowboy_req:read_urlencoded_body(Req0),
- ?assertMatch(
- #{
- <<"username">> := <<"user name">>,
- <<"clientid">> := <<"client id">>,
- <<"peerhost">> := <<"127.0.0.1">>,
- <<"proto_name">> := <<"MQTT">>,
- <<"mountpoint">> := <<"MOUNTPOINT">>,
- <<"topic">> := <<"t">>,
- <<"action">> := <<"publish">>,
- <<"CN">> := ?PH_CERT_CN_NAME,
- <<"CS">> := ?PH_CERT_SUBJECT
- },
- emqx_utils_json:decode(PostVars, [return_maps])
- ),
- {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
- end,
- #{
- <<"method">> => <<"post">>,
- <<"body">> => #{
- <<"username">> => <<"${username}">>,
- <<"clientid">> => <<"${clientid}">>,
- <<"peerhost">> => <<"${peerhost}">>,
- <<"proto_name">> => <<"${proto_name}">>,
- <<"mountpoint">> => <<"${mountpoint}">>,
- <<"topic">> => <<"${topic}">>,
- <<"action">> => <<"${action}">>,
- <<"CN">> => ?PH_CERT_CN_NAME,
- <<"CS">> => ?PH_CERT_SUBJECT
- },
- <<"headers">> => #{<<"content-type">> => <<"application/x-www-form-urlencoded">>}
- }
- ),
- ClientInfo = #{
- clientid => <<"client id">>,
- username => <<"user name">>,
- peerhost => {127, 0, 0, 1},
- protocol => <<"MQTT">>,
- mountpoint => <<"MOUNTPOINT">>,
- zone => default,
- listener => {tcp, default},
- cn => ?PH_CERT_CN_NAME,
- dn => ?PH_CERT_SUBJECT
- },
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ).
- t_no_value_for_placeholder(_Config) ->
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- ?assertEqual(
- <<"/authz/users/">>,
- cowboy_req:path(Req0)
- ),
- {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
- ?assertMatch(
- #{
- <<"mountpoint">> := <<"[]">>
- },
- emqx_utils_json:decode(RawBody, [return_maps])
- ),
- {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
- end,
- #{
- <<"method">> => <<"post">>,
- <<"body">> => #{
- <<"mountpoint">> => <<"[${mountpoint}]">>
- }
- }
- ),
- ClientInfo = #{
- clientid => <<"client id">>,
- username => <<"user name">>,
- peerhost => {127, 0, 0, 1},
- protocol => <<"MQTT">>,
- zone => default,
- listener => {tcp, default}
- },
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ).
- t_disallowed_placeholders_preserved(_Config) ->
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- {ok, Body, Req1} = cowboy_req:read_body(Req0),
- ?assertMatch(
- #{
- <<"cname">> := <<>>,
- <<"usertypo">> := <<"${usertypo}">>
- },
- emqx_utils_json:decode(Body)
- ),
- {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
- end,
- #{
- <<"method">> => <<"post">>,
- <<"body">> => #{
- <<"cname">> => ?PH_CERT_CN_NAME,
- <<"usertypo">> => <<"${usertypo}">>
- }
- }
- ),
- ClientInfo = #{
- clientid => <<"client id">>,
- username => <<"user name">>,
- peerhost => {127, 0, 0, 1},
- protocol => <<"MQTT">>,
- zone => default,
- listener => {tcp, default}
- },
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ).
- t_disallowed_placeholders_path(_Config) ->
- ok = setup_handler_and_config(
- fun(Req, State) ->
- {ok, ?AUTHZ_HTTP_RESP(allow, Req), State}
- end,
- #{
- <<"url">> => <<"http://127.0.0.1:33333/authz/use%20rs/${typo}">>
- }
- ),
- ClientInfo = #{
- clientid => <<"client id">>,
- username => <<"user name">>,
- peerhost => {127, 0, 0, 1},
- protocol => <<"MQTT">>,
- zone => default,
- listener => {tcp, default}
- },
- % % NOTE: disallowed placeholder left intact, which makes the URL invalid
- ?assertEqual(
- deny,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ).
- t_create_replace(_Config) ->
- ClientInfo = #{
- clientid => <<"clientid">>,
- username => <<"username">>,
- peerhost => {127, 0, 0, 1},
- zone => default,
- listener => {tcp, default}
- },
- %% Create with valid URL
- ok = setup_handler_and_config(
- fun(Req0, State) ->
- {ok, ?AUTHZ_HTTP_RESP(allow, Req0), State}
- end,
- #{
- <<"url">> =>
- <<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>
- }
- ),
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ),
- %% Changing to valid config
- OkConfig = maps:merge(
- raw_http_authz_config(),
- #{
- <<"url">> =>
- <<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>
- }
- ),
- ?assertMatch(
- {ok, _},
- emqx_authz:update({?CMD_REPLACE, http}, OkConfig)
- ),
- ?assertEqual(
- allow,
- emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
- ).
- %%------------------------------------------------------------------------------
- %% Helpers
- %%------------------------------------------------------------------------------
- raw_http_authz_config() ->
- #{
- <<"enable">> => <<"true">>,
- <<"type">> => <<"http">>,
- <<"method">> => <<"get">>,
- <<"url">> => <<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>,
- <<"headers">> => #{<<"X-Test-Header">> => <<"Test Value">>}
- }.
- setup_handler_and_config(Handler, Config) ->
- ok = emqx_authz_http_test_server:set_handler(Handler),
- ok = emqx_authz_test_lib:setup_config(
- raw_http_authz_config(),
- Config
- ).
- start_apps(Apps) ->
- lists:foreach(fun application:ensure_all_started/1, Apps).
- stop_apps(Apps) ->
- lists:foreach(fun application:stop/1, Apps).
|