emqx_authn_postgresql_SUITE.erl 20 KB

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