emqx_authn_pgsql_SUITE.erl 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  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. %%
  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_authn_pgsql_SUITE).
  17. -compile(nowarn_export_all).
  18. -compile(export_all).
  19. -include_lib("emqx_connector/include/emqx_connector.hrl").
  20. -include_lib("emqx_authn/include/emqx_authn.hrl").
  21. -include_lib("eunit/include/eunit.hrl").
  22. -include_lib("common_test/include/ct.hrl").
  23. -define(PGSQL_HOST, "pgsql").
  24. -define(PGSQL_RESOURCE, <<"emqx_authn_pgsql_SUITE">>).
  25. -define(ResourceID, <<"password_based:postgresql">>).
  26. -define(PATH, [authentication]).
  27. all() ->
  28. [
  29. {group, require_seeds},
  30. t_update_with_invalid_config,
  31. t_update_with_bad_config_value
  32. ].
  33. groups() ->
  34. [{require_seeds, [], [t_create, t_authenticate, t_update, t_destroy, t_is_superuser]}].
  35. init_per_testcase(_, Config) ->
  36. emqx_authentication:initialize_authentication(?GLOBAL, []),
  37. emqx_authn_test_lib:delete_authenticators(
  38. [authentication],
  39. ?GLOBAL
  40. ),
  41. Config.
  42. init_per_group(require_seeds, Config) ->
  43. ok = init_seeds(),
  44. Config.
  45. end_per_group(require_seeds, Config) ->
  46. ok = drop_seeds(),
  47. Config.
  48. init_per_suite(Config) ->
  49. case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
  50. true ->
  51. Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
  52. work_dir => ?config(priv_dir, Config)
  53. }),
  54. {ok, _} = emqx_resource:create_local(
  55. ?PGSQL_RESOURCE,
  56. ?RESOURCE_GROUP,
  57. emqx_connector_pgsql,
  58. pgsql_config(),
  59. #{}
  60. ),
  61. [{apps, Apps} | Config];
  62. false ->
  63. {skip, no_pgsql}
  64. end.
  65. end_per_suite(Config) ->
  66. emqx_authn_test_lib:delete_authenticators(
  67. [authentication],
  68. ?GLOBAL
  69. ),
  70. ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
  71. ok = emqx_cth_suite:stop(?config(apps, Config)),
  72. ok.
  73. %%------------------------------------------------------------------------------
  74. %% Tests
  75. %%------------------------------------------------------------------------------
  76. t_create(_Config) ->
  77. AuthConfig = raw_pgsql_auth_config(),
  78. {ok, _} = emqx:update_config(
  79. ?PATH,
  80. {create_authenticator, ?GLOBAL, AuthConfig}
  81. ),
  82. {ok, [#{provider := emqx_authn_pgsql}]} = emqx_authentication:list_authenticators(?GLOBAL),
  83. emqx_authn_test_lib:delete_config(?ResourceID).
  84. %% invalid config which does not pass the schema check should result in an error
  85. t_update_with_invalid_config(_Config) ->
  86. AuthConfig = raw_pgsql_auth_config(),
  87. BadConfig = maps:without([<<"server">>], AuthConfig),
  88. ?assertMatch(
  89. {error, #{
  90. kind := validation_error,
  91. matched_type := "authn:postgresql",
  92. path := "authentication.1.server",
  93. reason := required_field
  94. }},
  95. emqx:update_config(
  96. ?PATH,
  97. {create_authenticator, ?GLOBAL, BadConfig}
  98. )
  99. ),
  100. ok.
  101. %% bad config values may cause connection failure, but should still be able to update
  102. t_update_with_bad_config_value(_Config) ->
  103. AuthConfig = raw_pgsql_auth_config(),
  104. InvalidConfigs =
  105. [
  106. AuthConfig#{<<"server">> => <<"unknownhost:3333">>},
  107. AuthConfig#{<<"password">> => <<"wrongpass">>},
  108. AuthConfig#{<<"database">> => <<"wrongdatabase">>}
  109. ],
  110. lists:foreach(
  111. fun(Config) ->
  112. {ok, _} = emqx:update_config(
  113. ?PATH,
  114. {create_authenticator, ?GLOBAL, Config}
  115. ),
  116. emqx_authn_test_lib:delete_config(?ResourceID),
  117. ?assertEqual(
  118. {error, {not_found, {chain, ?GLOBAL}}},
  119. emqx_authentication:list_authenticators(?GLOBAL)
  120. )
  121. end,
  122. InvalidConfigs
  123. ).
  124. t_authenticate(_Config) ->
  125. ok = lists:foreach(
  126. fun(Sample) ->
  127. ct:pal("test_user_auth sample: ~p", [Sample]),
  128. test_user_auth(Sample)
  129. end,
  130. user_seeds()
  131. ).
  132. test_user_auth(#{
  133. credentials := Credentials0,
  134. config_params := SpecificConfigParams,
  135. result := Result
  136. }) ->
  137. AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfigParams),
  138. {ok, _} = emqx:update_config(
  139. ?PATH,
  140. {create_authenticator, ?GLOBAL, AuthConfig}
  141. ),
  142. Credentials = Credentials0#{
  143. listener => 'tcp:default',
  144. protocol => mqtt
  145. },
  146. ?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
  147. emqx_authn_test_lib:delete_authenticators(
  148. [authentication],
  149. ?GLOBAL
  150. ).
  151. t_destroy(_Config) ->
  152. AuthConfig = raw_pgsql_auth_config(),
  153. {ok, _} = emqx:update_config(
  154. ?PATH,
  155. {create_authenticator, ?GLOBAL, AuthConfig}
  156. ),
  157. {ok, [#{provider := emqx_authn_pgsql, state := State}]} =
  158. emqx_authentication:list_authenticators(?GLOBAL),
  159. {ok, _} = emqx_authn_pgsql:authenticate(
  160. #{
  161. username => <<"plain">>,
  162. password => <<"plain">>
  163. },
  164. State
  165. ),
  166. emqx_authn_test_lib:delete_authenticators(
  167. [authentication],
  168. ?GLOBAL
  169. ),
  170. % Authenticator should not be usable anymore
  171. ?assertMatch(
  172. ignore,
  173. emqx_authn_pgsql:authenticate(
  174. #{
  175. username => <<"plain">>,
  176. password => <<"plain">>
  177. },
  178. State
  179. )
  180. ).
  181. t_update(_Config) ->
  182. CorrectConfig = raw_pgsql_auth_config(),
  183. IncorrectConfig =
  184. CorrectConfig#{
  185. <<"query">> =>
  186. <<
  187. "SELECT password_hash, salt, is_superuser_str as is_superuser\n"
  188. " FROM users where username = ${username} LIMIT 0"
  189. >>
  190. },
  191. {ok, _} = emqx:update_config(
  192. ?PATH,
  193. {create_authenticator, ?GLOBAL, IncorrectConfig}
  194. ),
  195. {error, not_authorized} = emqx_access_control:authenticate(
  196. #{
  197. username => <<"plain">>,
  198. password => <<"plain">>,
  199. listener => 'tcp:default',
  200. protocol => mqtt
  201. }
  202. ),
  203. % We update with config with correct query, provider should update and work properly
  204. {ok, _} = emqx:update_config(
  205. ?PATH,
  206. {update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, CorrectConfig}
  207. ),
  208. {ok, _} = emqx_access_control:authenticate(
  209. #{
  210. username => <<"plain">>,
  211. password => <<"plain">>,
  212. listener => 'tcp:default',
  213. protocol => mqtt
  214. }
  215. ).
  216. t_is_superuser(_Config) ->
  217. Config = raw_pgsql_auth_config(),
  218. {ok, _} = emqx:update_config(
  219. ?PATH,
  220. {create_authenticator, ?GLOBAL, Config}
  221. ),
  222. Checks = [
  223. {is_superuser_str, "0", false},
  224. {is_superuser_str, "", false},
  225. {is_superuser_str, null, false},
  226. {is_superuser_str, "1", true},
  227. {is_superuser_str, "val", false},
  228. {is_superuser_int, 0, false},
  229. {is_superuser_int, null, false},
  230. {is_superuser_int, 1, true},
  231. {is_superuser_int, 123, true},
  232. {is_superuser_bool, false, false},
  233. {is_superuser_bool, null, false},
  234. {is_superuser_bool, true, true}
  235. ],
  236. lists:foreach(fun test_is_superuser/1, Checks).
  237. test_is_superuser({Field, Value, ExpectedValue}) ->
  238. {ok, _} = q("DELETE FROM users"),
  239. UserData = #{
  240. username => "user",
  241. password_hash => "plainsalt",
  242. salt => "salt",
  243. Field => Value
  244. },
  245. ok = create_user(UserData),
  246. Query =
  247. "SELECT password_hash, salt, " ++ atom_to_list(Field) ++
  248. " as is_superuser "
  249. "FROM users where username = ${username} LIMIT 1",
  250. Config = maps:put(<<"query">>, Query, raw_pgsql_auth_config()),
  251. {ok, _} = emqx:update_config(
  252. ?PATH,
  253. {update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, Config}
  254. ),
  255. Credentials = #{
  256. listener => 'tcp:default',
  257. protocol => mqtt,
  258. username => <<"user">>,
  259. password => <<"plain">>
  260. },
  261. ?assertEqual(
  262. {ok, #{is_superuser => ExpectedValue}},
  263. emqx_access_control:authenticate(Credentials)
  264. ).
  265. %%------------------------------------------------------------------------------
  266. %% Helpers
  267. %%------------------------------------------------------------------------------
  268. raw_pgsql_auth_config() ->
  269. #{
  270. <<"mechanism">> => <<"password_based">>,
  271. <<"password_hash_algorithm">> => #{
  272. <<"name">> => <<"plain">>,
  273. <<"salt_position">> => <<"suffix">>
  274. },
  275. <<"enable">> => <<"true">>,
  276. <<"backend">> => <<"postgresql">>,
  277. <<"database">> => <<"mqtt">>,
  278. <<"username">> => <<"root">>,
  279. <<"password">> => <<"public">>,
  280. <<"query">> =>
  281. <<
  282. "SELECT password_hash, salt, is_superuser_str as is_superuser\n"
  283. " FROM users where username = ${username} LIMIT 1"
  284. >>,
  285. <<"server">> => pgsql_server()
  286. }.
  287. user_seeds() ->
  288. [
  289. #{
  290. data => #{
  291. username => "plain",
  292. password_hash => "plainsalt",
  293. salt => "salt",
  294. is_superuser_str => "1"
  295. },
  296. credentials => #{
  297. username => <<"plain">>,
  298. password => <<"plain">>
  299. },
  300. config_params => #{},
  301. result => {ok, #{is_superuser => true}}
  302. },
  303. #{
  304. data => #{
  305. username => "md5",
  306. password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
  307. salt => "salt",
  308. is_superuser_str => "0"
  309. },
  310. credentials => #{
  311. username => <<"md5">>,
  312. password => <<"md5">>
  313. },
  314. config_params => #{
  315. <<"password_hash_algorithm">> => #{
  316. <<"name">> => <<"md5">>,
  317. <<"salt_position">> => <<"suffix">>
  318. }
  319. },
  320. result => {ok, #{is_superuser => false}}
  321. },
  322. #{
  323. data => #{
  324. username => "sha256",
  325. password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
  326. salt => "salt",
  327. is_superuser_int => 1
  328. },
  329. credentials => #{
  330. clientid => <<"sha256">>,
  331. password => <<"sha256">>
  332. },
  333. config_params => #{
  334. <<"query">> =>
  335. <<
  336. "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
  337. " FROM users where username = ${clientid} LIMIT 1"
  338. >>,
  339. <<"password_hash_algorithm">> => #{
  340. <<"name">> => <<"sha256">>,
  341. <<"salt_position">> => <<"prefix">>
  342. }
  343. },
  344. result => {ok, #{is_superuser => true}}
  345. },
  346. %% strip double quote support
  347. #{
  348. data => #{
  349. username => "sha256",
  350. password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
  351. salt => "salt",
  352. is_superuser_int => 1
  353. },
  354. credentials => #{
  355. username => <<"sha256">>,
  356. password => <<"sha256">>
  357. },
  358. config_params => #{
  359. <<"query">> =>
  360. <<
  361. "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
  362. " FROM users where username = \"${username}\" LIMIT 1"
  363. >>,
  364. <<"password_hash_algorithm">> => #{
  365. <<"name">> => <<"sha256">>,
  366. <<"salt_position">> => <<"prefix">>
  367. }
  368. },
  369. result => {ok, #{is_superuser => true}}
  370. },
  371. #{
  372. data => #{
  373. username => "sha256",
  374. password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
  375. cert_subject => <<"cert_subject_data">>,
  376. cert_common_name => <<"cert_common_name_data">>,
  377. salt => "salt",
  378. is_superuser_int => 1
  379. },
  380. credentials => #{
  381. clientid => <<"sha256">>,
  382. password => <<"sha256">>,
  383. cert_subject => <<"cert_subject_data">>,
  384. cert_common_name => <<"cert_common_name_data">>
  385. },
  386. config_params => #{
  387. <<"query">> =>
  388. <<
  389. "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
  390. " FROM users where cert_subject = ${cert_subject} AND \n"
  391. " cert_common_name = ${cert_common_name} LIMIT 1"
  392. >>,
  393. <<"password_hash_algorithm">> => #{
  394. <<"name">> => <<"sha256">>,
  395. <<"salt_position">> => <<"prefix">>
  396. }
  397. },
  398. result => {ok, #{is_superuser => true}}
  399. },
  400. #{
  401. data => #{
  402. username => <<"bcrypt">>,
  403. password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
  404. salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
  405. is_superuser_int => 0
  406. },
  407. credentials => #{
  408. username => <<"bcrypt">>,
  409. password => <<"bcrypt">>
  410. },
  411. config_params => #{
  412. <<"query">> =>
  413. <<
  414. "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
  415. " FROM users where username = ${username} LIMIT 1"
  416. >>,
  417. <<"password_hash_algorithm">> => #{<<"name">> => <<"bcrypt">>}
  418. },
  419. result => {ok, #{is_superuser => false}}
  420. },
  421. #{
  422. data => #{
  423. username => <<"bcrypt0">>,
  424. password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
  425. salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
  426. is_superuser_str => "0"
  427. },
  428. credentials => #{
  429. username => <<"bcrypt0">>,
  430. password => <<"bcrypt">>
  431. },
  432. config_params => #{
  433. % clientid variable & username credentials
  434. <<"query">> =>
  435. <<
  436. "SELECT password_hash, salt, is_superuser_int as is_superuser\n"
  437. " FROM users where username = ${clientid} LIMIT 1"
  438. >>,
  439. <<"password_hash_algorithm">> => #{<<"name">> => <<"bcrypt">>}
  440. },
  441. result => {error, not_authorized}
  442. },
  443. #{
  444. data => #{
  445. username => <<"bcrypt1">>,
  446. password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
  447. salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
  448. is_superuser_str => "0"
  449. },
  450. credentials => #{
  451. username => <<"bcrypt1">>,
  452. password => <<"bcrypt">>
  453. },
  454. config_params => #{
  455. % Bad keys in query
  456. <<"query">> =>
  457. <<
  458. "SELECT 1 AS unknown_field\n"
  459. " FROM users where username = ${username} LIMIT 1"
  460. >>,
  461. <<"password_hash_algorithm">> => #{<<"name">> => <<"bcrypt">>}
  462. },
  463. result => {error, not_authorized}
  464. },
  465. #{
  466. data => #{
  467. username => <<"bcrypt2">>,
  468. password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
  469. salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
  470. is_superuser => "0"
  471. },
  472. credentials => #{
  473. username => <<"bcrypt2">>,
  474. % Wrong password
  475. password => <<"wrongpass">>
  476. },
  477. config_params => #{
  478. <<"password_hash_algorithm">> => #{<<"name">> => <<"bcrypt">>}
  479. },
  480. result => {error, bad_username_or_password}
  481. }
  482. ].
  483. init_seeds() ->
  484. ok = drop_seeds(),
  485. {ok, _, _} = q(
  486. "CREATE TABLE users(\n"
  487. " username varchar(255),\n"
  488. " password_hash varchar(255),\n"
  489. " salt varchar(255),\n"
  490. " cert_subject varchar(255),\n"
  491. " cert_common_name varchar(255),\n"
  492. " is_superuser_str varchar(255),\n"
  493. " is_superuser_int smallint,\n"
  494. " is_superuser_bool boolean)"
  495. ),
  496. lists:foreach(
  497. fun(#{data := Values}) ->
  498. ok = create_user(Values)
  499. end,
  500. user_seeds()
  501. ).
  502. create_user(Values) ->
  503. Fields = [
  504. username,
  505. password_hash,
  506. salt,
  507. cert_subject,
  508. cert_common_name,
  509. is_superuser_str,
  510. is_superuser_int,
  511. is_superuser_bool
  512. ],
  513. InsertQuery =
  514. "INSERT INTO users(username, password_hash, salt, cert_subject, cert_common_name, "
  515. "is_superuser_str, is_superuser_int, is_superuser_bool) "
  516. "VALUES($1, $2, $3, $4, $5, $6, $7, $8)",
  517. Params = [maps:get(F, Values, null) || F <- Fields],
  518. {ok, 1} = q(InsertQuery, Params),
  519. ok.
  520. q(Sql) ->
  521. emqx_resource:simple_sync_query(
  522. ?PGSQL_RESOURCE,
  523. {query, Sql}
  524. ).
  525. q(Sql, Params) ->
  526. emqx_resource:simple_sync_query(
  527. ?PGSQL_RESOURCE,
  528. {query, Sql, Params}
  529. ).
  530. drop_seeds() ->
  531. {ok, _, _} = q("DROP TABLE IF EXISTS users"),
  532. ok.
  533. pgsql_server() ->
  534. iolist_to_binary(io_lib:format("~s", [?PGSQL_HOST])).
  535. pgsql_config() ->
  536. #{
  537. auto_reconnect => true,
  538. database => <<"mqtt">>,
  539. username => <<"root">>,
  540. password => <<"public">>,
  541. pool_size => 8,
  542. server => pgsql_server(),
  543. ssl => #{enable => false}
  544. }.
  545. start_apps(Apps) ->
  546. lists:foreach(fun application:ensure_all_started/1, Apps).
  547. stop_apps(Apps) ->
  548. lists:foreach(fun application:stop/1, Apps).