emqx_authn_mongodb_SUITE.erl 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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_mongodb_SUITE).
  17. -compile(nowarn_export_all).
  18. -compile(export_all).
  19. -include_lib("emqx_connector/include/emqx_connector.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(MONGO_HOST, "mongo").
  24. -define(MONGO_CLIENT, 'emqx_authn_mongo_SUITE_client').
  25. -define(PATH, [authentication]).
  26. all() ->
  27. emqx_common_test_helpers:all(?MODULE).
  28. init_per_testcase(_TestCase, Config) ->
  29. emqx_authn_test_lib:delete_authenticators(
  30. [authentication],
  31. ?GLOBAL
  32. ),
  33. {ok, _} = mc_worker_api:connect(mongo_config()),
  34. Config.
  35. end_per_testcase(_TestCase, _Config) ->
  36. ok = mc_worker_api:disconnect(?MONGO_CLIENT).
  37. init_per_suite(Config) ->
  38. case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
  39. true ->
  40. Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_auth, emqx_auth_mongodb], #{
  41. work_dir => ?config(priv_dir, Config)
  42. }),
  43. [{apps, Apps} | Config];
  44. false ->
  45. {skip, no_mongo}
  46. end.
  47. end_per_suite(Config) ->
  48. emqx_authn_test_lib:delete_authenticators(
  49. [authentication],
  50. ?GLOBAL
  51. ),
  52. ok = emqx_cth_suite:stop(?config(apps, Config)),
  53. ok.
  54. %%------------------------------------------------------------------------------
  55. %% Tests
  56. %%------------------------------------------------------------------------------
  57. t_create(_Config) ->
  58. AuthConfig = raw_mongo_auth_config(),
  59. {ok, _} = emqx:update_config(
  60. ?PATH,
  61. {create_authenticator, ?GLOBAL, AuthConfig}
  62. ),
  63. {ok, [#{provider := emqx_authn_mongodb}]} = emqx_authn_chains:list_authenticators(?GLOBAL).
  64. t_create_invalid(_Config) ->
  65. AuthConfig = raw_mongo_auth_config(),
  66. InvalidConfigs =
  67. [
  68. AuthConfig#{<<"mongo_type">> => <<"unknown">>},
  69. AuthConfig#{<<"filter">> => <<"{ \"username\": \"${username}\" }">>},
  70. AuthConfig#{<<"w_mode">> => <<"unknown">>}
  71. ],
  72. lists:foreach(
  73. fun(Config) ->
  74. {error, _} = emqx:update_config(
  75. ?PATH,
  76. {create_authenticator, ?GLOBAL, Config}
  77. ),
  78. ?assertEqual(
  79. {error, {not_found, {chain, ?GLOBAL}}},
  80. emqx_authn_chains:list_authenticators(?GLOBAL)
  81. )
  82. end,
  83. InvalidConfigs
  84. ).
  85. t_authenticate(_Config) ->
  86. ok = init_seeds(),
  87. ok = lists:foreach(
  88. fun(Sample) ->
  89. ct:pal("test_user_auth sample: ~p", [Sample]),
  90. test_user_auth(Sample)
  91. end,
  92. user_seeds()
  93. ),
  94. ok = drop_seeds().
  95. test_user_auth(#{
  96. credentials := Credentials0,
  97. config_params := SpecificConfigParams,
  98. result := Result
  99. }) ->
  100. AuthConfig = maps:merge(raw_mongo_auth_config(), SpecificConfigParams),
  101. {ok, _} = emqx:update_config(
  102. ?PATH,
  103. {create_authenticator, ?GLOBAL, AuthConfig}
  104. ),
  105. Credentials = Credentials0#{
  106. listener => 'tcp:default',
  107. protocol => mqtt
  108. },
  109. ?assertEqual(Result, emqx_access_control:authenticate(Credentials)),
  110. emqx_authn_test_lib:delete_authenticators(
  111. [authentication],
  112. ?GLOBAL
  113. ).
  114. t_destroy(_Config) ->
  115. ok = init_seeds(),
  116. AuthConfig = raw_mongo_auth_config(),
  117. {ok, _} = emqx:update_config(
  118. ?PATH,
  119. {create_authenticator, ?GLOBAL, AuthConfig}
  120. ),
  121. {ok, [#{provider := emqx_authn_mongodb, state := State}]} =
  122. emqx_authn_chains:list_authenticators(?GLOBAL),
  123. {ok, _} = emqx_authn_mongodb:authenticate(
  124. #{
  125. username => <<"plain">>,
  126. password => <<"plain">>
  127. },
  128. State
  129. ),
  130. emqx_authn_test_lib:delete_authenticators(
  131. [authentication],
  132. ?GLOBAL
  133. ),
  134. % Authenticator should not be usable anymore
  135. ?assertMatch(
  136. ignore,
  137. emqx_authn_mongodb:authenticate(
  138. #{
  139. username => <<"plain">>,
  140. password => <<"plain">>
  141. },
  142. State
  143. )
  144. ),
  145. ok = drop_seeds().
  146. t_update(_Config) ->
  147. ok = init_seeds(),
  148. CorrectConfig = raw_mongo_auth_config(),
  149. IncorrectConfig =
  150. CorrectConfig#{<<"filter">> => #{<<"wrongfield">> => <<"wrongvalue">>}},
  151. {ok, _} = emqx:update_config(
  152. ?PATH,
  153. {create_authenticator, ?GLOBAL, IncorrectConfig}
  154. ),
  155. {error, not_authorized} = emqx_access_control:authenticate(
  156. #{
  157. username => <<"plain">>,
  158. password => <<"plain">>,
  159. listener => 'tcp:default',
  160. protocol => mqtt
  161. }
  162. ),
  163. % We update with config with correct filter, provider should update and work properly
  164. {ok, _} = emqx:update_config(
  165. ?PATH,
  166. {update_authenticator, ?GLOBAL, <<"password_based:mongodb">>, CorrectConfig}
  167. ),
  168. {ok, _} = emqx_access_control:authenticate(
  169. #{
  170. username => <<"plain">>,
  171. password => <<"plain">>,
  172. listener => 'tcp:default',
  173. protocol => mqtt
  174. }
  175. ),
  176. ok = drop_seeds().
  177. t_is_superuser(_Config) ->
  178. Config = raw_mongo_auth_config(),
  179. {ok, _} = emqx:update_config(
  180. ?PATH,
  181. {create_authenticator, ?GLOBAL, Config}
  182. ),
  183. Checks = [
  184. {<<"0">>, false},
  185. {<<"">>, false},
  186. {null, false},
  187. {false, false},
  188. {0, false},
  189. {<<"val">>, false},
  190. {<<"1">>, true},
  191. {<<"123">>, true},
  192. {1, true},
  193. {123, true},
  194. {true, true}
  195. ],
  196. lists:foreach(fun test_is_superuser/1, Checks).
  197. test_is_superuser({Value, ExpectedValue}) ->
  198. {true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"users">>, #{}),
  199. UserData = #{
  200. username => <<"user">>,
  201. password_hash => <<"plainsalt">>,
  202. salt => <<"salt">>,
  203. is_superuser => Value
  204. },
  205. {{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, [UserData]),
  206. Credentials = #{
  207. listener => 'tcp:default',
  208. protocol => mqtt,
  209. username => <<"user">>,
  210. password => <<"plain">>
  211. },
  212. ?assertEqual(
  213. {ok, #{is_superuser => ExpectedValue}},
  214. emqx_access_control:authenticate(Credentials)
  215. ).
  216. %%------------------------------------------------------------------------------
  217. %% Helpers
  218. %%------------------------------------------------------------------------------
  219. raw_mongo_auth_config() ->
  220. #{
  221. <<"mechanism">> => <<"password_based">>,
  222. <<"password_hash_algorithm">> => #{
  223. <<"name">> => <<"plain">>,
  224. <<"salt_position">> => <<"suffix">>
  225. },
  226. <<"enable">> => <<"true">>,
  227. <<"backend">> => <<"mongodb">>,
  228. <<"mongo_type">> => <<"single">>,
  229. <<"database">> => <<"mqtt">>,
  230. <<"collection">> => <<"users">>,
  231. <<"server">> => mongo_server(),
  232. <<"w_mode">> => <<"unsafe">>,
  233. <<"auth_source">> => mongo_authsource(),
  234. <<"username">> => mongo_username(),
  235. <<"password">> => mongo_password(),
  236. <<"filter">> => #{<<"username">> => <<"${username}">>},
  237. <<"password_hash_field">> => <<"password_hash">>,
  238. <<"salt_field">> => <<"salt">>,
  239. <<"is_superuser_field">> => <<"is_superuser">>
  240. }.
  241. user_seeds() ->
  242. [
  243. #{
  244. data => #{
  245. username => <<"plain">>,
  246. password_hash => <<"plainsalt">>,
  247. salt => <<"salt">>,
  248. is_superuser => <<"1">>
  249. },
  250. credentials => #{
  251. username => <<"plain">>,
  252. password => <<"plain">>
  253. },
  254. config_params => #{},
  255. result => {ok, #{is_superuser => true}}
  256. },
  257. #{
  258. data => #{
  259. username => <<"md5">>,
  260. password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>,
  261. salt => <<"salt">>,
  262. is_superuser => <<"0">>
  263. },
  264. credentials => #{
  265. username => <<"md5">>,
  266. password => <<"md5">>
  267. },
  268. config_params => #{
  269. <<"password_hash_algorithm">> => #{
  270. <<"name">> => <<"md5">>,
  271. <<"salt_position">> => <<"suffix">>
  272. }
  273. },
  274. result => {ok, #{is_superuser => false}}
  275. },
  276. #{
  277. data => #{
  278. username => <<"sha256">>,
  279. password_hash =>
  280. <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
  281. salt => <<"salt">>,
  282. is_superuser => 1
  283. },
  284. credentials => #{
  285. clientid => <<"sha256">>,
  286. password => <<"sha256">>
  287. },
  288. config_params => #{
  289. <<"filter">> => #{<<"username">> => <<"${clientid}">>},
  290. <<"password_hash_algorithm">> => #{
  291. <<"name">> => <<"sha256">>,
  292. <<"salt_position">> => <<"prefix">>
  293. }
  294. },
  295. result => {ok, #{is_superuser => true}}
  296. },
  297. #{
  298. data => #{
  299. cert_subject => <<"cert_subject_data">>,
  300. cert_common_name => <<"cert_common_name_data">>,
  301. password_hash =>
  302. <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>,
  303. salt => <<"salt">>,
  304. is_superuser => 1
  305. },
  306. credentials => #{
  307. cert_subject => <<"cert_subject_data">>,
  308. cert_common_name => <<"cert_common_name_data">>,
  309. password => <<"sha256">>
  310. },
  311. config_params => #{
  312. <<"filter">> => #{
  313. <<"cert_subject">> => <<"${cert_subject}">>,
  314. <<"cert_common_name">> => <<"${cert_common_name}">>
  315. },
  316. <<"password_hash_algorithm">> => #{
  317. <<"name">> => <<"sha256">>,
  318. <<"salt_position">> => <<"prefix">>
  319. }
  320. },
  321. result => {ok, #{is_superuser => true}}
  322. },
  323. #{
  324. data => #{
  325. username => <<"bcrypt">>,
  326. password_hash =>
  327. <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
  328. salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
  329. is_superuser => 0
  330. },
  331. credentials => #{
  332. username => <<"bcrypt">>,
  333. password => <<"bcrypt">>
  334. },
  335. config_params => #{
  336. <<"password_hash_algorithm">> => #{<<"name">> => <<"bcrypt">>}
  337. },
  338. result => {ok, #{is_superuser => false}}
  339. },
  340. #{
  341. data => #{
  342. username => <<"bcrypt0">>,
  343. password_hash =>
  344. <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
  345. salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
  346. is_superuser => <<"0">>
  347. },
  348. credentials => #{
  349. username => <<"bcrypt0">>,
  350. password => <<"bcrypt">>
  351. },
  352. config_params => #{
  353. % clientid variable & username credentials
  354. <<"filter">> => #{<<"username">> => <<"${clientid}">>},
  355. <<"password_hash_algorithm">> => #{<<"name">> => <<"bcrypt">>}
  356. },
  357. result => {error, not_authorized}
  358. },
  359. #{
  360. data => #{
  361. username => <<"bcrypt1">>,
  362. password_hash =>
  363. <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
  364. salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
  365. is_superuser => <<"0">>
  366. },
  367. credentials => #{
  368. username => <<"bcrypt1">>,
  369. password => <<"bcrypt">>
  370. },
  371. config_params => #{
  372. <<"filter">> => #{<<"userid">> => <<"${clientid}">>},
  373. <<"password_hash_algorithm">> => #{<<"name">> => <<"bcrypt">>}
  374. },
  375. result => {error, not_authorized}
  376. },
  377. #{
  378. data => #{
  379. username => <<"bcrypt2">>,
  380. password_hash =>
  381. <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>,
  382. salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>,
  383. is_superuser => <<"0">>
  384. },
  385. credentials => #{
  386. username => <<"bcrypt2">>,
  387. % Wrong password
  388. password => <<"wrongpass">>
  389. },
  390. config_params => #{
  391. <<"password_hash_algorithm">> => #{<<"name">> => <<"bcrypt">>}
  392. },
  393. result => {error, bad_username_or_password}
  394. }
  395. ].
  396. init_seeds() ->
  397. Users = [Values || #{data := Values} <- user_seeds()],
  398. {{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, Users),
  399. ok.
  400. drop_seeds() ->
  401. {true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"users">>, #{}),
  402. ok.
  403. mongo_server() ->
  404. iolist_to_binary(io_lib:format("~s", [?MONGO_HOST])).
  405. mongo_config() ->
  406. [
  407. {database, <<"mqtt">>},
  408. {host, ?MONGO_HOST},
  409. {port, ?MONGO_DEFAULT_PORT},
  410. {auth_source, mongo_authsource()},
  411. {login, mongo_username()},
  412. {password, mongo_password()},
  413. {register, ?MONGO_CLIENT}
  414. ].
  415. mongo_authsource() ->
  416. iolist_to_binary(os:getenv("MONGO_AUTHSOURCE", "admin")).
  417. mongo_username() ->
  418. iolist_to_binary(os:getenv("MONGO_USERNAME", "")).
  419. mongo_password() ->
  420. iolist_to_binary(os:getenv("MONGO_PASSWORD", "")).
  421. start_apps(Apps) ->
  422. lists:foreach(fun application:ensure_all_started/1, Apps).
  423. stop_apps(Apps) ->
  424. lists:foreach(fun application:stop/1, Apps).