emqx_authz_http_SUITE.erl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. -module(emqx_authz_http_SUITE).
  17. -compile(nowarn_export_all).
  18. -compile(export_all).
  19. -include_lib("emqx_auth/include/emqx_authz.hrl").
  20. -include_lib("eunit/include/eunit.hrl").
  21. -include_lib("common_test/include/ct.hrl").
  22. -include_lib("emqx/include/emqx_placeholder.hrl").
  23. -include_lib("snabbkaffe/include/snabbkaffe.hrl").
  24. -define(HTTP_PORT, 33333).
  25. -define(HTTP_PATH, "/authz/[...]").
  26. -define(AUTHZ_HTTP_RESP(Result, Req),
  27. cowboy_req:reply(
  28. 200,
  29. #{<<"content-type">> => <<"application/json">>},
  30. "{\"result\": \"" ++ atom_to_list(Result) ++ "\"}",
  31. Req
  32. )
  33. ).
  34. all() ->
  35. emqx_common_test_helpers:all(?MODULE).
  36. init_per_suite(Config) ->
  37. Apps = emqx_cth_suite:start(
  38. [
  39. emqx,
  40. {emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"},
  41. emqx_auth,
  42. emqx_auth_http
  43. ],
  44. #{work_dir => ?config(priv_dir, Config)}
  45. ),
  46. [{suite_apps, Apps} | Config].
  47. end_per_suite(_Config) ->
  48. ok = emqx_authz_test_lib:restore_authorizers(),
  49. emqx_cth_suite:stop(?config(suite_apps, _Config)).
  50. init_per_testcase(_Case, Config) ->
  51. ok = emqx_authz_test_lib:reset_authorizers(),
  52. {ok, _} = emqx_authz_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH),
  53. Config.
  54. end_per_testcase(_Case, _Config) ->
  55. _ = emqx_authz:set_feature_available(rich_actions, true),
  56. try
  57. ok = emqx_authz_http_test_server:stop()
  58. catch
  59. exit:noproc ->
  60. ok
  61. end,
  62. snabbkaffe:stop(),
  63. ok.
  64. %%------------------------------------------------------------------------------
  65. %% Tests
  66. %%------------------------------------------------------------------------------
  67. t_response_handling(_Config) ->
  68. ClientInfo = #{
  69. clientid => <<"clientid">>,
  70. username => <<"username">>,
  71. peerhost => {127, 0, 0, 1},
  72. zone => default,
  73. listener => {tcp, default}
  74. },
  75. %% OK, get, body & headers
  76. ok = setup_handler_and_config(
  77. fun(Req0, State) ->
  78. {ok, ?AUTHZ_HTTP_RESP(allow, Req0), State}
  79. end,
  80. #{}
  81. ),
  82. ?assertEqual(
  83. allow,
  84. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  85. ),
  86. %% Not OK, get, no body
  87. ok = setup_handler_and_config(
  88. fun(Req0, State) ->
  89. Req = cowboy_req:reply(200, Req0),
  90. {ok, Req, State}
  91. end,
  92. #{}
  93. ),
  94. deny = emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>),
  95. %% OK, get, 204
  96. ok = setup_handler_and_config(
  97. fun(Req0, State) ->
  98. Req = cowboy_req:reply(204, Req0),
  99. {ok, Req, State}
  100. end,
  101. #{}
  102. ),
  103. ?assertEqual(
  104. allow,
  105. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  106. ),
  107. %% Not OK, get, 400
  108. ok = setup_handler_and_config(
  109. fun(Req0, State) ->
  110. Req = cowboy_req:reply(400, Req0),
  111. {ok, Req, State}
  112. end,
  113. #{}
  114. ),
  115. ?assertEqual(
  116. deny,
  117. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  118. ),
  119. %% Not OK, get, 400 + body & headers
  120. ok = setup_handler_and_config(
  121. fun(Req0, State) ->
  122. Req = cowboy_req:reply(
  123. 400,
  124. #{<<"content-type">> => <<"text/plain">>},
  125. "Response body",
  126. Req0
  127. ),
  128. {ok, Req, State}
  129. end,
  130. #{}
  131. ),
  132. ?assertEqual(
  133. deny,
  134. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  135. ),
  136. %% the server cannot be reached; should skip to the next
  137. %% authorizer in the chain.
  138. ok = emqx_authz_http_test_server:stop(),
  139. ?check_trace(
  140. ?assertEqual(
  141. deny,
  142. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  143. ),
  144. fun(Trace) ->
  145. ?assertMatch(
  146. [
  147. #{
  148. ?snk_kind := authz_http_request_failure,
  149. error := {recoverable_error, econnrefused}
  150. }
  151. ],
  152. ?of_kind(authz_http_request_failure, Trace)
  153. ),
  154. ?assert(
  155. ?strict_causality(
  156. #{?snk_kind := authz_http_request_failure},
  157. #{?snk_kind := authz_non_superuser, result := nomatch},
  158. Trace
  159. )
  160. ),
  161. ok
  162. end
  163. ),
  164. ok.
  165. t_query_params(_Config) ->
  166. ok = setup_handler_and_config(
  167. fun(Req0, State) ->
  168. #{
  169. username := <<"user name">>,
  170. clientid := <<"client id">>,
  171. peerhost := <<"127.0.0.1">>,
  172. proto_name := <<"MQTT">>,
  173. mountpoint := <<"MOUNTPOINT">>,
  174. topic := <<"t/1">>,
  175. action := <<"publish">>,
  176. qos := <<"1">>,
  177. retain := <<"false">>
  178. } = cowboy_req:match_qs(
  179. [
  180. username,
  181. clientid,
  182. peerhost,
  183. proto_name,
  184. mountpoint,
  185. topic,
  186. action,
  187. qos,
  188. retain
  189. ],
  190. Req0
  191. ),
  192. {ok, ?AUTHZ_HTTP_RESP(allow, Req0), State}
  193. end,
  194. #{
  195. <<"url">> => <<
  196. "http://127.0.0.1:33333/authz/users/?"
  197. "username=${username}&"
  198. "clientid=${clientid}&"
  199. "peerhost=${peerhost}&"
  200. "proto_name=${proto_name}&"
  201. "mountpoint=${mountpoint}&"
  202. "topic=${topic}&"
  203. "action=${action}&"
  204. "qos=${qos}&"
  205. "retain=${retain}"
  206. >>
  207. }
  208. ),
  209. ClientInfo = #{
  210. clientid => <<"client id">>,
  211. username => <<"user name">>,
  212. peerhost => {127, 0, 0, 1},
  213. protocol => <<"MQTT">>,
  214. mountpoint => <<"MOUNTPOINT">>,
  215. zone => default,
  216. listener => {tcp, default}
  217. },
  218. ?assertEqual(
  219. allow,
  220. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t/1">>)
  221. ).
  222. t_path(_Config) ->
  223. ok = setup_handler_and_config(
  224. fun(Req0, State) ->
  225. ?assertEqual(
  226. <<
  227. "/authz/use%20rs/"
  228. "user%20name/"
  229. "client%20id/"
  230. "127.0.0.1/"
  231. "MQTT/"
  232. "MOUNTPOINT/"
  233. "t%2F1/"
  234. "publish/"
  235. "1/"
  236. "false"
  237. >>,
  238. cowboy_req:path(Req0)
  239. ),
  240. {ok, ?AUTHZ_HTTP_RESP(allow, Req0), State}
  241. end,
  242. #{
  243. <<"url">> => <<
  244. "http://127.0.0.1:33333/authz/use%20rs/"
  245. "${username}/"
  246. "${clientid}/"
  247. "${peerhost}/"
  248. "${proto_name}/"
  249. "${mountpoint}/"
  250. "${topic}/"
  251. "${action}/"
  252. "${qos}/"
  253. "${retain}"
  254. >>
  255. }
  256. ),
  257. ClientInfo = #{
  258. clientid => <<"client id">>,
  259. username => <<"user name">>,
  260. peerhost => {127, 0, 0, 1},
  261. protocol => <<"MQTT">>,
  262. mountpoint => <<"MOUNTPOINT">>,
  263. zone => default,
  264. listener => {tcp, default}
  265. },
  266. ?assertEqual(
  267. allow,
  268. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t/1">>)
  269. ).
  270. t_json_body(_Config) ->
  271. ok = setup_handler_and_config(
  272. fun(Req0, State) ->
  273. ?assertEqual(
  274. <<"/authz/users/">>,
  275. cowboy_req:path(Req0)
  276. ),
  277. {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
  278. ?assertMatch(
  279. #{
  280. <<"username">> := <<"user name">>,
  281. <<"CLIENT">> := <<"client id">>,
  282. <<"peerhost">> := <<"127.0.0.1">>,
  283. <<"proto_name">> := <<"MQTT">>,
  284. <<"mountpoint">> := <<"MOUNTPOINT">>,
  285. <<"topic">> := <<"t">>,
  286. <<"action">> := <<"publish">>,
  287. <<"qos">> := <<"1">>,
  288. <<"retain">> := <<"false">>
  289. },
  290. emqx_utils_json:decode(RawBody, [return_maps])
  291. ),
  292. {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
  293. end,
  294. #{
  295. <<"method">> => <<"post">>,
  296. <<"body">> => #{
  297. <<"username">> => <<"${username}">>,
  298. <<"CLIENT">> => <<"${clientid}">>,
  299. <<"peerhost">> => <<"${peerhost}">>,
  300. <<"proto_name">> => <<"${proto_name}">>,
  301. <<"mountpoint">> => <<"${mountpoint}">>,
  302. <<"topic">> => <<"${topic}">>,
  303. <<"action">> => <<"${action}">>,
  304. <<"qos">> => <<"${qos}">>,
  305. <<"retain">> => <<"${retain}">>
  306. }
  307. }
  308. ),
  309. ClientInfo = #{
  310. clientid => <<"client id">>,
  311. username => <<"user name">>,
  312. peerhost => {127, 0, 0, 1},
  313. protocol => <<"MQTT">>,
  314. mountpoint => <<"MOUNTPOINT">>,
  315. zone => default,
  316. listener => {tcp, default}
  317. },
  318. ?assertEqual(
  319. allow,
  320. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t">>)
  321. ).
  322. t_no_rich_actions(_Config) ->
  323. _ = emqx_authz:set_feature_available(rich_actions, false),
  324. ok = setup_handler_and_config(
  325. fun(Req0, State) ->
  326. ?assertEqual(
  327. <<"/authz/users/">>,
  328. cowboy_req:path(Req0)
  329. ),
  330. {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
  331. %% No interpolation if rich_actions is disabled
  332. ?assertMatch(
  333. #{
  334. <<"qos">> := <<"${qos}">>,
  335. <<"retain">> := <<"${retain}">>
  336. },
  337. emqx_utils_json:decode(RawBody, [return_maps])
  338. ),
  339. {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
  340. end,
  341. #{
  342. <<"method">> => <<"post">>,
  343. <<"body">> => #{
  344. <<"qos">> => <<"${qos}">>,
  345. <<"retain">> => <<"${retain}">>
  346. }
  347. }
  348. ),
  349. ClientInfo = emqx_authz_test_lib:base_client_info(),
  350. ?assertEqual(
  351. allow,
  352. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH(1, false), <<"t">>)
  353. ).
  354. t_placeholder_and_body(_Config) ->
  355. ok = setup_handler_and_config(
  356. fun(Req0, State) ->
  357. ?assertEqual(
  358. <<"/authz/users/">>,
  359. cowboy_req:path(Req0)
  360. ),
  361. {ok, [{PostVars, true}], Req1} = cowboy_req:read_urlencoded_body(Req0),
  362. ?assertMatch(
  363. #{
  364. <<"username">> := <<"user name">>,
  365. <<"clientid">> := <<"client id">>,
  366. <<"peerhost">> := <<"127.0.0.1">>,
  367. <<"proto_name">> := <<"MQTT">>,
  368. <<"mountpoint">> := <<"MOUNTPOINT">>,
  369. <<"topic">> := <<"t">>,
  370. <<"action">> := <<"publish">>,
  371. <<"CN">> := ?PH_CERT_CN_NAME,
  372. <<"CS">> := ?PH_CERT_SUBJECT
  373. },
  374. emqx_utils_json:decode(PostVars, [return_maps])
  375. ),
  376. {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
  377. end,
  378. #{
  379. <<"method">> => <<"post">>,
  380. <<"body">> => #{
  381. <<"username">> => <<"${username}">>,
  382. <<"clientid">> => <<"${clientid}">>,
  383. <<"peerhost">> => <<"${peerhost}">>,
  384. <<"proto_name">> => <<"${proto_name}">>,
  385. <<"mountpoint">> => <<"${mountpoint}">>,
  386. <<"topic">> => <<"${topic}">>,
  387. <<"action">> => <<"${action}">>,
  388. <<"CN">> => ?PH_CERT_CN_NAME,
  389. <<"CS">> => ?PH_CERT_SUBJECT
  390. },
  391. <<"headers">> => #{<<"content-type">> => <<"application/x-www-form-urlencoded">>}
  392. }
  393. ),
  394. ClientInfo = #{
  395. clientid => <<"client id">>,
  396. username => <<"user name">>,
  397. peerhost => {127, 0, 0, 1},
  398. protocol => <<"MQTT">>,
  399. mountpoint => <<"MOUNTPOINT">>,
  400. zone => default,
  401. listener => {tcp, default},
  402. cn => ?PH_CERT_CN_NAME,
  403. dn => ?PH_CERT_SUBJECT
  404. },
  405. ?assertEqual(
  406. allow,
  407. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  408. ).
  409. t_no_value_for_placeholder(_Config) ->
  410. ok = setup_handler_and_config(
  411. fun(Req0, State) ->
  412. ?assertEqual(
  413. <<"/authz/users/">>,
  414. cowboy_req:path(Req0)
  415. ),
  416. {ok, RawBody, Req1} = cowboy_req:read_body(Req0),
  417. ?assertMatch(
  418. #{
  419. <<"mountpoint">> := <<"[]">>
  420. },
  421. emqx_utils_json:decode(RawBody, [return_maps])
  422. ),
  423. {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
  424. end,
  425. #{
  426. <<"method">> => <<"post">>,
  427. <<"body">> => #{
  428. <<"mountpoint">> => <<"[${mountpoint}]">>
  429. }
  430. }
  431. ),
  432. ClientInfo = #{
  433. clientid => <<"client id">>,
  434. username => <<"user name">>,
  435. peerhost => {127, 0, 0, 1},
  436. protocol => <<"MQTT">>,
  437. zone => default,
  438. listener => {tcp, default}
  439. },
  440. ?assertEqual(
  441. allow,
  442. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  443. ).
  444. t_disallowed_placeholders_preserved(_Config) ->
  445. ok = setup_handler_and_config(
  446. fun(Req0, State) ->
  447. {ok, Body, Req1} = cowboy_req:read_body(Req0),
  448. ?assertMatch(
  449. #{
  450. <<"cname">> := <<>>,
  451. <<"usertypo">> := <<"${usertypo}">>
  452. },
  453. emqx_utils_json:decode(Body)
  454. ),
  455. {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
  456. end,
  457. #{
  458. <<"method">> => <<"post">>,
  459. <<"body">> => #{
  460. <<"cname">> => ?PH_CERT_CN_NAME,
  461. <<"usertypo">> => <<"${usertypo}">>
  462. }
  463. }
  464. ),
  465. ClientInfo = #{
  466. clientid => <<"client id">>,
  467. username => <<"user name">>,
  468. peerhost => {127, 0, 0, 1},
  469. protocol => <<"MQTT">>,
  470. zone => default,
  471. listener => {tcp, default}
  472. },
  473. ?assertEqual(
  474. allow,
  475. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  476. ).
  477. t_disallowed_placeholders_path(_Config) ->
  478. ok = setup_handler_and_config(
  479. fun(Req, State) ->
  480. {ok, ?AUTHZ_HTTP_RESP(allow, Req), State}
  481. end,
  482. #{
  483. <<"url">> => <<"http://127.0.0.1:33333/authz/use%20rs/${typo}">>
  484. }
  485. ),
  486. ClientInfo = #{
  487. clientid => <<"client id">>,
  488. username => <<"user name">>,
  489. peerhost => {127, 0, 0, 1},
  490. protocol => <<"MQTT">>,
  491. zone => default,
  492. listener => {tcp, default}
  493. },
  494. % % NOTE: disallowed placeholder left intact, which makes the URL invalid
  495. ?assertEqual(
  496. deny,
  497. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  498. ).
  499. t_create_replace(_Config) ->
  500. ClientInfo = #{
  501. clientid => <<"clientid">>,
  502. username => <<"username">>,
  503. peerhost => {127, 0, 0, 1},
  504. zone => default,
  505. listener => {tcp, default}
  506. },
  507. %% Create with valid URL
  508. ok = setup_handler_and_config(
  509. fun(Req0, State) ->
  510. {ok, ?AUTHZ_HTTP_RESP(allow, Req0), State}
  511. end,
  512. #{
  513. <<"url">> =>
  514. <<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>
  515. }
  516. ),
  517. ?assertEqual(
  518. allow,
  519. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  520. ),
  521. %% Changing to valid config
  522. OkConfig = maps:merge(
  523. raw_http_authz_config(),
  524. #{
  525. <<"url">> =>
  526. <<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>
  527. }
  528. ),
  529. ?assertMatch(
  530. {ok, _},
  531. emqx_authz:update({?CMD_REPLACE, http}, OkConfig)
  532. ),
  533. ?assertEqual(
  534. allow,
  535. emqx_access_control:authorize(ClientInfo, ?AUTHZ_PUBLISH, <<"t">>)
  536. ).
  537. %%------------------------------------------------------------------------------
  538. %% Helpers
  539. %%------------------------------------------------------------------------------
  540. raw_http_authz_config() ->
  541. #{
  542. <<"enable">> => <<"true">>,
  543. <<"type">> => <<"http">>,
  544. <<"method">> => <<"get">>,
  545. <<"url">> => <<"http://127.0.0.1:33333/authz/users/?topic=${topic}&action=${action}">>,
  546. <<"headers">> => #{<<"X-Test-Header">> => <<"Test Value">>}
  547. }.
  548. setup_handler_and_config(Handler, Config) ->
  549. ok = emqx_authz_http_test_server:set_handler(Handler),
  550. ok = emqx_authz_test_lib:setup_config(
  551. raw_http_authz_config(),
  552. Config
  553. ).
  554. start_apps(Apps) ->
  555. lists:foreach(fun application:ensure_all_started/1, Apps).
  556. stop_apps(Apps) ->
  557. lists:foreach(fun application:stop/1, Apps).