emqx_authn_pgsql_SUITE.erl 19 KB

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