emqx_authn_pgsql_SUITE.erl 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  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("emqx_connector.hrl").
  20. -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. Config.
  39. init_per_group(require_seeds, Config) ->
  40. ok = init_seeds(),
  41. Config.
  42. end_per_group(require_seeds, Config) ->
  43. ok = drop_seeds(),
  44. Config.
  45. init_per_suite(Config) ->
  46. _ = application:load(emqx_conf),
  47. case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
  48. true ->
  49. ok = emqx_common_test_helpers:start_apps([emqx_authn]),
  50. ok = start_apps([emqx_resource, emqx_connector]),
  51. {ok, _} = emqx_resource:create_local(
  52. ?PGSQL_RESOURCE,
  53. ?RESOURCE_GROUP,
  54. emqx_connector_pgsql,
  55. pgsql_config(),
  56. #{}),
  57. Config;
  58. false ->
  59. {skip, no_pgsql}
  60. end.
  61. end_per_suite(_Config) ->
  62. emqx_authn_test_lib:delete_authenticators(
  63. [authentication],
  64. ?GLOBAL),
  65. ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
  66. ok = stop_apps([emqx_resource, emqx_connector]),
  67. ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
  68. %%------------------------------------------------------------------------------
  69. %% Tests
  70. %%------------------------------------------------------------------------------
  71. t_create(_Config) ->
  72. AuthConfig = raw_pgsql_auth_config(),
  73. {ok, _} = emqx:update_config(
  74. ?PATH,
  75. {create_authenticator, ?GLOBAL, AuthConfig}),
  76. {ok, [#{provider := emqx_authn_pgsql}]} = emqx_authentication:list_authenticators(?GLOBAL),
  77. emqx_authn_test_lib:delete_config(?ResourceID).
  78. t_create_invalid(_Config) ->
  79. AuthConfig = raw_pgsql_auth_config(),
  80. InvalidConfigs =
  81. [
  82. maps:without([server], AuthConfig),
  83. AuthConfig#{server => <<"unknownhost:3333">>},
  84. AuthConfig#{password => <<"wrongpass">>},
  85. AuthConfig#{database => <<"wrongdatabase">>}
  86. ],
  87. lists:foreach(
  88. fun(Config) ->
  89. {ok, _} = emqx:update_config(
  90. ?PATH,
  91. {create_authenticator, ?GLOBAL, Config}),
  92. emqx_authn_test_lib:delete_config(?ResourceID),
  93. {ok, []} = emqx_authentication:list_authenticators(?GLOBAL)
  94. end,
  95. InvalidConfigs).
  96. t_authenticate(_Config) ->
  97. ok = lists:foreach(
  98. fun(Sample) ->
  99. ct:pal("test_user_auth sample: ~p", [Sample]),
  100. test_user_auth(Sample)
  101. end,
  102. user_seeds()).
  103. test_user_auth(#{credentials := Credentials0,
  104. config_params := SpecificConfigParams,
  105. result := Result}) ->
  106. AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfigParams),
  107. {ok, _} = emqx:update_config(
  108. ?PATH,
  109. {create_authenticator, ?GLOBAL, AuthConfig}),
  110. Credentials = Credentials0#{
  111. listener => 'tcp:default',
  112. protocol => mqtt
  113. },
  114. ?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
  115. emqx_authn_test_lib:delete_authenticators(
  116. [authentication],
  117. ?GLOBAL).
  118. t_destroy(_Config) ->
  119. AuthConfig = raw_pgsql_auth_config(),
  120. {ok, _} = emqx:update_config(
  121. ?PATH,
  122. {create_authenticator, ?GLOBAL, AuthConfig}),
  123. {ok, [#{provider := emqx_authn_pgsql, state := State}]}
  124. = emqx_authentication:list_authenticators(?GLOBAL),
  125. {ok, _} = emqx_authn_pgsql:authenticate(
  126. #{username => <<"plain">>,
  127. password => <<"plain">>
  128. },
  129. State),
  130. emqx_authn_test_lib:delete_authenticators(
  131. [authentication],
  132. ?GLOBAL),
  133. % Authenticator should not be usable anymore
  134. ?assertMatch(
  135. ignore,
  136. emqx_authn_pgsql:authenticate(
  137. #{username => <<"plain">>,
  138. password => <<"plain">>
  139. },
  140. State)).
  141. t_update(_Config) ->
  142. CorrectConfig = raw_pgsql_auth_config(),
  143. IncorrectConfig =
  144. CorrectConfig#{
  145. query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
  146. FROM users where username = ${username} LIMIT 0">>},
  147. {ok, _} = emqx:update_config(
  148. ?PATH,
  149. {create_authenticator, ?GLOBAL, IncorrectConfig}),
  150. {error, not_authorized} = emqx_access_control:authenticate(
  151. #{username => <<"plain">>,
  152. password => <<"plain">>,
  153. listener => 'tcp:default',
  154. protocol => mqtt
  155. }),
  156. % We update with config with correct query, provider should update and work properly
  157. {ok, _} = emqx:update_config(
  158. ?PATH,
  159. {update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, CorrectConfig}),
  160. {ok,_} = emqx_access_control:authenticate(
  161. #{username => <<"plain">>,
  162. password => <<"plain">>,
  163. listener => 'tcp:default',
  164. protocol => mqtt
  165. }).
  166. t_is_superuser(_Config) ->
  167. Config = raw_pgsql_auth_config(),
  168. {ok, _} = emqx:update_config(
  169. ?PATH,
  170. {create_authenticator, ?GLOBAL, Config}),
  171. Checks = [
  172. {is_superuser_str, "0", false},
  173. {is_superuser_str, "", false},
  174. {is_superuser_str, null, false},
  175. {is_superuser_str, "1", true},
  176. {is_superuser_str, "val", true},
  177. {is_superuser_int, 0, false},
  178. {is_superuser_int, null, false},
  179. {is_superuser_int, 1, true},
  180. {is_superuser_int, 123, true},
  181. {is_superuser_bool, false, false},
  182. {is_superuser_bool, null, false},
  183. {is_superuser_bool, true, true}
  184. ],
  185. lists:foreach(fun test_is_superuser/1, Checks).
  186. test_is_superuser({Field, Value, ExpectedValue}) ->
  187. {ok, _} = q("DELETE FROM users"),
  188. UserData = #{
  189. username => "user",
  190. password_hash => "plainsalt",
  191. salt => "salt",
  192. Field => Value
  193. },
  194. ok = create_user(UserData),
  195. Query = "SELECT password_hash, salt, " ++ atom_to_list(Field) ++ " as is_superuser "
  196. "FROM users where username = ${username} LIMIT 1",
  197. Config = maps:put(query, Query, raw_pgsql_auth_config()),
  198. {ok, _} = emqx:update_config(
  199. ?PATH,
  200. {update_authenticator, ?GLOBAL, <<"password_based:postgresql">>, Config}),
  201. Credentials = #{
  202. listener => 'tcp:default',
  203. protocol => mqtt,
  204. username => <<"user">>,
  205. password => <<"plain">>
  206. },
  207. ?assertEqual(
  208. {ok, #{is_superuser => ExpectedValue}},
  209. emqx_access_control:authenticate(Credentials)).
  210. %%------------------------------------------------------------------------------
  211. %% Helpers
  212. %%------------------------------------------------------------------------------
  213. raw_pgsql_auth_config() ->
  214. #{
  215. mechanism => <<"password_based">>,
  216. password_hash_algorithm => #{name => <<"plain">>,
  217. salt_position => <<"suffix">>},
  218. enable => <<"true">>,
  219. backend => <<"postgresql">>,
  220. database => <<"mqtt">>,
  221. username => <<"root">>,
  222. password => <<"public">>,
  223. query => <<"SELECT password_hash, salt, is_superuser_str as is_superuser
  224. FROM users where username = ${username} LIMIT 1">>,
  225. server => pgsql_server()
  226. }.
  227. user_seeds() ->
  228. [#{data => #{
  229. username => "plain",
  230. password_hash => "plainsalt",
  231. salt => "salt",
  232. is_superuser_str => "1"
  233. },
  234. credentials => #{
  235. username => <<"plain">>,
  236. password => <<"plain">>},
  237. config_params => #{},
  238. result => {ok,#{is_superuser => true}}
  239. },
  240. #{data => #{
  241. username => "md5",
  242. password_hash => "9b4d0c43d206d48279e69b9ad7132e22",
  243. salt => "salt",
  244. is_superuser_str => "0"
  245. },
  246. credentials => #{
  247. username => <<"md5">>,
  248. password => <<"md5">>
  249. },
  250. config_params => #{
  251. password_hash_algorithm => #{name => <<"md5">>,
  252. salt_position => <<"suffix">>}
  253. },
  254. result => {ok,#{is_superuser => false}}
  255. },
  256. #{data => #{
  257. username => "sha256",
  258. password_hash => "ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf",
  259. salt => "salt",
  260. is_superuser_int => 1
  261. },
  262. credentials => #{
  263. clientid => <<"sha256">>,
  264. password => <<"sha256">>
  265. },
  266. config_params => #{
  267. query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
  268. FROM users where username = ${clientid} LIMIT 1">>,
  269. password_hash_algorithm => #{name => <<"sha256">>,
  270. salt_position => <<"prefix">>}
  271. },
  272. result => {ok,#{is_superuser => true}}
  273. },
  274. #{data => #{
  275. username => <<"bcrypt">>,
  276. password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
  277. salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
  278. is_superuser_int => 0
  279. },
  280. credentials => #{
  281. username => <<"bcrypt">>,
  282. password => <<"bcrypt">>
  283. },
  284. config_params => #{
  285. query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
  286. FROM users where username = ${username} LIMIT 1">>,
  287. password_hash_algorithm => #{name => <<"bcrypt">>}
  288. },
  289. result => {ok,#{is_superuser => false}}
  290. },
  291. #{data => #{
  292. username => <<"bcrypt0">>,
  293. password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
  294. salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
  295. is_superuser_str => "0"
  296. },
  297. credentials => #{
  298. username => <<"bcrypt0">>,
  299. password => <<"bcrypt">>
  300. },
  301. config_params => #{
  302. % clientid variable & username credentials
  303. query => <<"SELECT password_hash, salt, is_superuser_int as is_superuser
  304. FROM users where username = ${clientid} LIMIT 1">>,
  305. password_hash_algorithm => #{name => <<"bcrypt">>}
  306. },
  307. result => {error,not_authorized}
  308. },
  309. #{data => #{
  310. username => <<"bcrypt1">>,
  311. password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
  312. salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
  313. is_superuser_str => "0"
  314. },
  315. credentials => #{
  316. username => <<"bcrypt1">>,
  317. password => <<"bcrypt">>
  318. },
  319. config_params => #{
  320. % Bad keys in query
  321. query => <<"SELECT 1 AS unknown_field
  322. FROM users where username = ${username} LIMIT 1">>,
  323. password_hash_algorithm => #{name => <<"bcrypt">>}
  324. },
  325. result => {error,not_authorized}
  326. },
  327. #{data => #{
  328. username => <<"bcrypt2">>,
  329. password_hash => "$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u",
  330. salt => "$2b$12$wtY3h20mUjjmeaClpqZVve",
  331. is_superuser => "0"
  332. },
  333. credentials => #{
  334. username => <<"bcrypt2">>,
  335. % Wrong password
  336. password => <<"wrongpass">>
  337. },
  338. config_params => #{
  339. password_hash_algorithm => #{name => <<"bcrypt">>}
  340. },
  341. result => {error,bad_username_or_password}
  342. }
  343. ].
  344. init_seeds() ->
  345. ok = drop_seeds(),
  346. {ok, _, _} = q("CREATE TABLE users(
  347. username varchar(255),
  348. password_hash varchar(255),
  349. salt varchar(255),
  350. is_superuser_str varchar(255),
  351. is_superuser_int smallint,
  352. is_superuser_bool boolean)"),
  353. lists:foreach(
  354. fun(#{data := Values}) ->
  355. ok = create_user(Values)
  356. end,
  357. user_seeds()).
  358. create_user(Values) ->
  359. Fields = [username, password_hash, salt, is_superuser_str, is_superuser_int, is_superuser_bool],
  360. InsertQuery = "INSERT INTO users(username, password_hash, salt,"
  361. "is_superuser_str, is_superuser_int, is_superuser_bool) "
  362. "VALUES($1, $2, $3, $4, $5, $6)",
  363. Params = [maps:get(F, Values, null) || F <- Fields],
  364. {ok, 1} = q(InsertQuery, Params),
  365. ok.
  366. q(Sql) ->
  367. emqx_resource:query(
  368. ?PGSQL_RESOURCE,
  369. {query, Sql}).
  370. q(Sql, Params) ->
  371. emqx_resource:query(
  372. ?PGSQL_RESOURCE,
  373. {query, Sql, Params}).
  374. drop_seeds() ->
  375. {ok, _, _} = q("DROP TABLE IF EXISTS users"),
  376. ok.
  377. pgsql_server() ->
  378. iolist_to_binary(io_lib:format("~s",[?PGSQL_HOST])).
  379. pgsql_config() ->
  380. #{auto_reconnect => true,
  381. database => <<"mqtt">>,
  382. username => <<"root">>,
  383. password => <<"public">>,
  384. pool_size => 8,
  385. server => {?PGSQL_HOST, ?PGSQL_DEFAULT_PORT},
  386. ssl => #{enable => false}
  387. }.
  388. start_apps(Apps) ->
  389. lists:foreach(fun application:ensure_all_started/1, Apps).
  390. stop_apps(Apps) ->
  391. lists:foreach(fun application:stop/1, Apps).