emqx_authn_pgsql_SUITE.erl 18 KB

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