emqx_authn_postgresql_SUITE.erl 20 KB

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