emqx_authn_postgresql_SUITE.erl 19 KB

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