emqx_authz_postgresql_SUITE.erl 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-2023 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. %% http://www.apache.org/licenses/LICENSE-2.0
  8. %%
  9. %% Unless required by applicable law or agreed to in writing, software
  10. %% distributed under the License is distributed on an "AS IS" BASIS,
  11. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. %% See the License for the specific language governing permissions and
  13. %% limitations under the License.
  14. %%--------------------------------------------------------------------
  15. -module(emqx_authz_postgresql_SUITE).
  16. -compile(nowarn_export_all).
  17. -compile(export_all).
  18. -include("emqx_connector.hrl").
  19. -include("emqx_authz.hrl").
  20. -include_lib("eunit/include/eunit.hrl").
  21. -include_lib("common_test/include/ct.hrl").
  22. -define(PGSQL_HOST, "pgsql").
  23. -define(PGSQL_RESOURCE, <<"emqx_authz_pgsql_SUITE">>).
  24. all() ->
  25. emqx_authz_test_lib:all_with_table_case(?MODULE, t_run_case, cases()).
  26. groups() ->
  27. emqx_authz_test_lib:table_groups(t_run_case, cases()).
  28. init_per_suite(Config) ->
  29. ok = stop_apps([emqx_resource]),
  30. case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
  31. true ->
  32. ok = emqx_common_test_helpers:start_apps(
  33. [emqx_conf, emqx_authz],
  34. fun set_special_configs/1
  35. ),
  36. ok = start_apps([emqx_resource]),
  37. {ok, _} = create_pgsql_resource(),
  38. Config;
  39. false ->
  40. {skip, no_pgsql}
  41. end.
  42. end_per_suite(_Config) ->
  43. ok = emqx_authz_test_lib:restore_authorizers(),
  44. ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
  45. ok = stop_apps([emqx_resource]),
  46. ok = emqx_common_test_helpers:stop_apps([emqx_conf, emqx_authz]).
  47. init_per_group(Group, Config) ->
  48. [{test_case, emqx_authz_test_lib:get_case(Group, cases())} | Config].
  49. end_per_group(_Group, _Config) ->
  50. ok.
  51. init_per_testcase(_TestCase, Config) ->
  52. ok = emqx_authz_test_lib:reset_authorizers(),
  53. Config.
  54. end_per_testcase(_TestCase, _Config) ->
  55. _ = emqx_authz:set_feature_available(rich_actions, true),
  56. ok = drop_table(),
  57. ok.
  58. set_special_configs(emqx_authz) ->
  59. ok = emqx_authz_test_lib:reset_authorizers();
  60. set_special_configs(_) ->
  61. ok.
  62. %%------------------------------------------------------------------------------
  63. %% Testcases
  64. %%------------------------------------------------------------------------------
  65. t_run_case(Config) ->
  66. Case = ?config(test_case, Config),
  67. ok = setup_source_data(Case),
  68. ok = setup_authz_source(Case),
  69. ok = emqx_authz_test_lib:run_checks(Case).
  70. t_create_invalid(_Config) ->
  71. BadConfig = maps:merge(
  72. raw_pgsql_authz_config(),
  73. #{<<"server">> => <<"255.255.255.255:33333">>}
  74. ),
  75. {ok, _} = emqx_authz:update(?CMD_REPLACE, [BadConfig]),
  76. [_] = emqx_authz:lookup().
  77. %%------------------------------------------------------------------------------
  78. %% Cases
  79. %%------------------------------------------------------------------------------
  80. cases() ->
  81. [
  82. #{
  83. name => base_publish,
  84. setup => [
  85. "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
  86. "permission VARCHAR(255), action VARCHAR(255))",
  87. "INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'allow', 'publish')",
  88. "INSERT INTO acl(username, topic, permission, action) VALUES('username', 'b', 'allow', 'subscribe')"
  89. ],
  90. query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
  91. client_info => #{username => <<"username">>},
  92. checks => [
  93. {allow, ?AUTHZ_PUBLISH, <<"a">>},
  94. {deny, ?AUTHZ_PUBLISH, <<"b">>},
  95. {deny, ?AUTHZ_SUBSCRIBE, <<"a">>},
  96. {allow, ?AUTHZ_SUBSCRIBE, <<"b">>}
  97. ]
  98. },
  99. #{
  100. name => rule_by_clientid_cn_dn_peerhost,
  101. setup => [
  102. "CREATE TABLE acl(clientid VARCHAR(255), cn VARCHAR(255), dn VARCHAR(255),"
  103. " peerhost VARCHAR(255), topic VARCHAR(255),"
  104. " permission VARCHAR(255), action VARCHAR(255))",
  105. "INSERT INTO acl(clientid, cn, dn, peerhost, topic, permission, action)"
  106. " VALUES('clientid', 'cn', 'dn', '127.0.0.1', 'a', 'allow', 'publish')"
  107. ],
  108. query =>
  109. "SELECT permission, action, topic FROM acl WHERE"
  110. " clientid = ${clientid} AND cn = ${cert_common_name}"
  111. " AND dn = ${cert_subject} AND peerhost = ${peerhost}",
  112. client_info => #{
  113. clientid => <<"clientid">>,
  114. cn => <<"cn">>,
  115. dn => <<"dn">>,
  116. peerhost => {127, 0, 0, 1}
  117. },
  118. checks => [
  119. {allow, ?AUTHZ_PUBLISH, <<"a">>},
  120. {deny, ?AUTHZ_PUBLISH, <<"b">>}
  121. ]
  122. },
  123. #{
  124. name => topics_literal_wildcard_variable,
  125. setup => [
  126. "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
  127. "permission VARCHAR(255), action VARCHAR(255))",
  128. "INSERT INTO acl(username, topic, permission, action) "
  129. "VALUES('username', 't/${username}', 'allow', 'publish')",
  130. "INSERT INTO acl(username, topic, permission, action) "
  131. "VALUES('username', 't/${clientid}', 'allow', 'publish')",
  132. "INSERT INTO acl(username, topic, permission, action) "
  133. "VALUES('username', 'eq t/${username}', 'allow', 'publish')",
  134. "INSERT INTO acl(username, topic, permission, action) "
  135. "VALUES('username', 't/#', 'allow', 'publish')",
  136. "INSERT INTO acl(username, topic, permission, action) "
  137. "VALUES('username', 't1/+', 'allow', 'publish')"
  138. ],
  139. query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
  140. client_info => #{
  141. username => <<"username">>
  142. },
  143. checks => [
  144. {allow, ?AUTHZ_PUBLISH, <<"t/username">>},
  145. {allow, ?AUTHZ_PUBLISH, <<"t/clientid">>},
  146. {allow, ?AUTHZ_PUBLISH, <<"t/${username}">>},
  147. {allow, ?AUTHZ_PUBLISH, <<"t/1/2">>},
  148. {allow, ?AUTHZ_PUBLISH, <<"t1/1">>},
  149. {deny, ?AUTHZ_PUBLISH, <<"t1/1/2">>},
  150. {deny, ?AUTHZ_PUBLISH, <<"abc">>},
  151. {deny, ?AUTHZ_SUBSCRIBE, <<"t/username">>}
  152. ]
  153. },
  154. #{
  155. name => qos_retain_in_query_result,
  156. features => [rich_actions],
  157. setup => [
  158. "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
  159. "permission VARCHAR(255), action VARCHAR(255),"
  160. "qos_s VARCHAR(255), retain_s VARCHAR(255))",
  161. "INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
  162. " VALUES('username', 't1', 'allow', 'publish', '1', 'true')",
  163. "INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
  164. " VALUES('username', 't2', 'allow', 'publish', '2', 'false')",
  165. "INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
  166. " VALUES('username', 't3', 'allow', 'publish', '0,1,2', 'all')",
  167. "INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
  168. " VALUES('username', 't4', 'allow', 'subscribe', '1', null)",
  169. "INSERT INTO acl(username, topic, permission, action, qos_s, retain_s)"
  170. " VALUES('username', 't5', 'allow', 'subscribe', '0,1,2', null)"
  171. ],
  172. query =>
  173. "SELECT permission, action, topic, qos_s as qos, retain_s as retain"
  174. " FROM acl WHERE username = ${username}",
  175. client_info => #{
  176. username => <<"username">>
  177. },
  178. checks => [
  179. {allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
  180. {deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
  181. {deny, ?AUTHZ_PUBLISH(0, true), <<"t1">>},
  182. {allow, ?AUTHZ_PUBLISH(2, false), <<"t2">>},
  183. {deny, ?AUTHZ_PUBLISH(1, false), <<"t2">>},
  184. {deny, ?AUTHZ_PUBLISH(2, true), <<"t2">>},
  185. {allow, ?AUTHZ_PUBLISH(1, true), <<"t3">>},
  186. {allow, ?AUTHZ_PUBLISH(2, false), <<"t3">>},
  187. {allow, ?AUTHZ_PUBLISH(2, true), <<"t3">>},
  188. {allow, ?AUTHZ_PUBLISH(0, false), <<"t3">>},
  189. {allow, ?AUTHZ_SUBSCRIBE(1), <<"t4">>},
  190. {deny, ?AUTHZ_SUBSCRIBE(2), <<"t4">>},
  191. {allow, ?AUTHZ_SUBSCRIBE(1), <<"t5">>},
  192. {allow, ?AUTHZ_SUBSCRIBE(2), <<"t5">>},
  193. {allow, ?AUTHZ_SUBSCRIBE(0), <<"t5">>}
  194. ]
  195. },
  196. #{
  197. name => qos_retain_in_query_result_as_integer,
  198. features => [rich_actions],
  199. setup => [
  200. "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
  201. "permission VARCHAR(255), action VARCHAR(255),"
  202. "qos_i VARCHAR(255), retain_i VARCHAR(255))",
  203. "INSERT INTO acl(username, topic, permission, action, qos_i, retain_i)"
  204. " VALUES('username', 't1', 'allow', 'publish', 1, 1)"
  205. ],
  206. query =>
  207. "SELECT permission, action, topic, qos_i as qos, retain_i as retain"
  208. " FROM acl WHERE username = ${username}",
  209. client_info => #{
  210. username => <<"username">>
  211. },
  212. checks => [
  213. {allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
  214. {deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
  215. {deny, ?AUTHZ_PUBLISH(0, true), <<"t1">>}
  216. ]
  217. },
  218. #{
  219. name => retain_in_query_result_as_boolean,
  220. features => [rich_actions],
  221. setup => [
  222. "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), permission VARCHAR(255),"
  223. " action VARCHAR(255), retain_b BOOLEAN)",
  224. "INSERT INTO acl(username, topic, permission, action, retain_b)"
  225. " VALUES('username', 't1', 'allow', 'publish', true)",
  226. "INSERT INTO acl(username, topic, permission, action, retain_b)"
  227. " VALUES('username', 't2', 'allow', 'publish', false)"
  228. ],
  229. query =>
  230. "SELECT permission, action, topic, retain_b as retain"
  231. " FROM acl WHERE username = ${username}",
  232. client_info => #{
  233. username => <<"username">>
  234. },
  235. checks => [
  236. {allow, ?AUTHZ_PUBLISH(1, true), <<"t1">>},
  237. {deny, ?AUTHZ_PUBLISH(1, false), <<"t1">>},
  238. {allow, ?AUTHZ_PUBLISH(1, false), <<"t2">>},
  239. {deny, ?AUTHZ_PUBLISH(1, true), <<"t2">>}
  240. ]
  241. },
  242. #{
  243. name => nonbin_values_in_client_info,
  244. setup => [
  245. "CREATE TABLE acl(who VARCHAR(255), topic VARCHAR(255), permission VARCHAR(255),"
  246. " action VARCHAR(255))",
  247. "INSERT INTO acl(who, topic, permission, action)"
  248. " VALUES('username', 't/${username}', 'allow', 'publish')",
  249. "INSERT INTO acl(who, topic, permission, action)"
  250. " VALUES('clientid', 't/${clientid}', 'allow', 'publish')"
  251. ],
  252. query =>
  253. "SELECT permission, action, topic"
  254. " FROM acl WHERE who = ${username} OR who = ${clientid}",
  255. client_info => #{
  256. %% string, not a binary
  257. username => "username",
  258. %% atom, not a binary
  259. clientid => clientid
  260. },
  261. checks => [
  262. {allow, ?AUTHZ_PUBLISH, <<"t/username">>},
  263. {allow, ?AUTHZ_PUBLISH, <<"t/clientid">>},
  264. {deny, ?AUTHZ_PUBLISH, <<"t/foo">>}
  265. ]
  266. },
  267. #{
  268. name => array_null_qos,
  269. features => [rich_actions],
  270. setup => [
  271. "CREATE TABLE acl(qos INTEGER[], "
  272. " topic VARCHAR(255), permission VARCHAR(255), action VARCHAR(255))",
  273. "INSERT INTO acl(qos, topic, permission, action)"
  274. " VALUES('{1,2}', 'tp', 'allow', 'publish')",
  275. "INSERT INTO acl(qos, topic, permission, action)"
  276. " VALUES(NULL, 'ts', 'allow', 'subscribe')"
  277. ],
  278. query =>
  279. "SELECT permission, action, topic, qos FROM acl",
  280. checks => [
  281. {allow, ?AUTHZ_PUBLISH(1, false), <<"tp">>},
  282. {allow, ?AUTHZ_PUBLISH(2, false), <<"tp">>},
  283. {deny, ?AUTHZ_PUBLISH(3, false), <<"tp">>},
  284. {allow, ?AUTHZ_SUBSCRIBE(1), <<"ts">>},
  285. {allow, ?AUTHZ_SUBSCRIBE(2), <<"ts">>}
  286. ]
  287. },
  288. #{
  289. name => strip_double_quote,
  290. setup => [
  291. "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
  292. "permission VARCHAR(255), action VARCHAR(255))",
  293. "INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'allow', 'publish')"
  294. ],
  295. query => "SELECT permission, action, topic FROM acl WHERE username = \"${username}\"",
  296. checks => [
  297. {allow, ?AUTHZ_PUBLISH, <<"a">>}
  298. ]
  299. },
  300. #{
  301. name => invalid_query,
  302. setup => [],
  303. query => "SELECT permission, action, topic FROM acl WHER",
  304. checks => [
  305. {deny, ?AUTHZ_PUBLISH, <<"a">>}
  306. ]
  307. },
  308. #{
  309. name => pgsql_error,
  310. setup => [],
  311. query =>
  312. "SELECT permission, action, topic FROM table_not_exists WHERE username = ${username}",
  313. checks => [
  314. {deny, ?AUTHZ_PUBLISH, <<"t">>}
  315. ]
  316. },
  317. #{
  318. name => invalid_rule,
  319. setup => [
  320. "CREATE TABLE acl(username VARCHAR(255), topic VARCHAR(255), "
  321. "permission VARCHAR(255), action VARCHAR(255))",
  322. %% 'permit' is invalid value for action
  323. "INSERT INTO acl(username, topic, permission, action) VALUES('username', 'a', 'permit', 'publish')"
  324. ],
  325. query => "SELECT permission, action, topic FROM acl WHERE username = ${username}",
  326. checks => [
  327. {deny, ?AUTHZ_PUBLISH, <<"a">>}
  328. ]
  329. }
  330. %% TODO: add case for unknown variables after fixing EMQX-10400
  331. ].
  332. %%------------------------------------------------------------------------------
  333. %% Helpers
  334. %%------------------------------------------------------------------------------
  335. setup_source_data(#{setup := Queries}) ->
  336. lists:foreach(
  337. fun(Query) ->
  338. _ = q(Query)
  339. end,
  340. Queries
  341. ).
  342. setup_authz_source(#{query := Query}) ->
  343. setup_config(
  344. #{
  345. <<"query">> => Query
  346. }
  347. ).
  348. raw_pgsql_authz_config() ->
  349. #{
  350. <<"enable">> => <<"true">>,
  351. <<"type">> => <<"postgresql">>,
  352. <<"database">> => <<"mqtt">>,
  353. <<"username">> => <<"root">>,
  354. <<"password">> => <<"public">>,
  355. <<"query">> => <<
  356. "SELECT permission, action, topic "
  357. "FROM acl WHERE username = ${username}"
  358. >>,
  359. <<"server">> => <<?PGSQL_HOST>>
  360. }.
  361. q(Sql) ->
  362. emqx_resource:simple_sync_query(
  363. ?PGSQL_RESOURCE,
  364. {query, Sql}
  365. ).
  366. drop_table() ->
  367. {ok, _, _} = q("DROP TABLE IF EXISTS acl"),
  368. ok.
  369. setup_config(SpecialParams) ->
  370. emqx_authz_test_lib:setup_config(
  371. raw_pgsql_authz_config(),
  372. SpecialParams
  373. ).
  374. pgsql_config() ->
  375. #{
  376. auto_reconnect => true,
  377. database => <<"mqtt">>,
  378. username => <<"root">>,
  379. password => <<"public">>,
  380. pool_size => 8,
  381. server => <<?PGSQL_HOST>>,
  382. ssl => #{enable => false}
  383. }.
  384. create_pgsql_resource() ->
  385. emqx_resource:create_local(
  386. ?PGSQL_RESOURCE,
  387. ?RESOURCE_GROUP,
  388. emqx_connector_pgsql,
  389. pgsql_config(),
  390. #{}
  391. ).
  392. start_apps(Apps) ->
  393. lists:foreach(fun application:ensure_all_started/1, Apps).
  394. stop_apps(Apps) ->
  395. lists:foreach(fun application:stop/1, Apps).