emqx_authz_api_sources_SUITE.erl 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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. %% http://www.apache.org/licenses/LICENSE-2.0
  8. %%
  9. %% Unless required by applicable law or agreed to in writing, software
  10. %% distributed under the License is distributed on an "AS IS" BASIS,
  11. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. %% See the License for the specific language governing permissions and
  13. %% limitations under the License.
  14. %%--------------------------------------------------------------------
  15. -module(emqx_authz_api_sources_SUITE).
  16. -compile(nowarn_export_all).
  17. -compile(export_all).
  18. -include("emqx_authz.hrl").
  19. -include_lib("eunit/include/eunit.hrl").
  20. -include_lib("common_test/include/ct.hrl").
  21. -include_lib("emqx/include/emqx_placeholder.hrl").
  22. -define(HOST, "http://127.0.0.1:18083/").
  23. -define(API_VERSION, "v5").
  24. -define(BASE_PATH, "api").
  25. -define(MONGO_SINGLE_HOST, "mongo:27017").
  26. -define(SOURCE1, #{<<"type">> => <<"http">>,
  27. <<"enable">> => true,
  28. <<"url">> => <<"https://fake.com:443/acl?username=", ?PH_USERNAME/binary>>,
  29. <<"headers">> => #{},
  30. <<"method">> => <<"get">>,
  31. <<"request_timeout">> => <<"5s">>
  32. }).
  33. -define(SOURCE2, #{<<"type">> => <<"mongodb">>,
  34. <<"enable">> => true,
  35. <<"mongo_type">> => <<"single">>,
  36. <<"server">> => <<?MONGO_SINGLE_HOST>>,
  37. <<"w_mode">> => <<"unsafe">>,
  38. <<"pool_size">> => 1,
  39. <<"database">> => <<"mqtt">>,
  40. <<"ssl">> => #{<<"enable">> => false},
  41. <<"collection">> => <<"fake">>,
  42. <<"selector">> => #{<<"a">> => <<"b">>}
  43. }).
  44. -define(SOURCE3, #{<<"type">> => <<"mysql">>,
  45. <<"enable">> => true,
  46. <<"server">> => <<"mysql:3306">>,
  47. <<"pool_size">> => 1,
  48. <<"database">> => <<"mqtt">>,
  49. <<"username">> => <<"xx">>,
  50. <<"password">> => <<"ee">>,
  51. <<"auto_reconnect">> => true,
  52. <<"ssl">> => #{<<"enable">> => false},
  53. <<"query">> => <<"abcb">>
  54. }).
  55. -define(SOURCE4, #{<<"type">> => <<"postgresql">>,
  56. <<"enable">> => true,
  57. <<"server">> => <<"pgsql:5432">>,
  58. <<"pool_size">> => 1,
  59. <<"database">> => <<"mqtt">>,
  60. <<"username">> => <<"xx">>,
  61. <<"password">> => <<"ee">>,
  62. <<"auto_reconnect">> => true,
  63. <<"ssl">> => #{<<"enable">> => false},
  64. <<"query">> => <<"abcb">>
  65. }).
  66. -define(SOURCE5, #{<<"type">> => <<"redis">>,
  67. <<"enable">> => true,
  68. <<"servers">> => <<"redis:6379,127.0.0.1:6380">>,
  69. <<"pool_size">> => 1,
  70. <<"database">> => 0,
  71. <<"password">> => <<"ee">>,
  72. <<"auto_reconnect">> => true,
  73. <<"ssl">> => #{<<"enable">> => false},
  74. <<"cmd">> => <<"HGETALL mqtt_authz:", ?PH_USERNAME/binary>>
  75. }).
  76. -define(SOURCE6, #{<<"type">> => <<"file">>,
  77. <<"enable">> => true,
  78. <<"rules">> =>
  79. <<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}."
  80. "\n{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
  81. }).
  82. -define(MATCH_RSA_KEY, <<"-----BEGIN RSA PRIVATE KEY", _/binary>>).
  83. -define(MATCH_CERT, <<"-----BEGIN CERTIFICATE", _/binary>>).
  84. all() ->
  85. emqx_common_test_helpers:all(?MODULE).
  86. groups() ->
  87. [].
  88. init_per_suite(Config) ->
  89. meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
  90. meck:expect(emqx_resource, create_local, fun(_, _, _) -> {ok, meck_data} end),
  91. meck:expect(emqx_resource, create_dry_run_local,
  92. fun(emqx_connector_mysql, _) -> ok;
  93. (emqx_connector_mongo, _) -> ok;
  94. (T, C) -> meck:passthrough([T, C])
  95. end),
  96. meck:expect(emqx_resource, health_check, fun(St) -> {ok, St} end),
  97. meck:expect(emqx_resource, remove_local, fun(_) -> ok end ),
  98. ok = emqx_common_test_helpers:start_apps(
  99. [emqx_conf, emqx_authz, emqx_dashboard],
  100. fun set_special_configs/1),
  101. Config.
  102. end_per_suite(_Config) ->
  103. {ok, _} = emqx:update_config(
  104. [authorization],
  105. #{<<"no_match">> => <<"allow">>,
  106. <<"cache">> => #{<<"enable">> => <<"true">>},
  107. <<"sources">> => []}),
  108. emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
  109. meck:unload(emqx_resource),
  110. ok.
  111. set_special_configs(emqx_dashboard) ->
  112. Config = #{
  113. default_username => <<"admin">>,
  114. default_password => <<"public">>,
  115. listeners => [#{
  116. protocol => http,
  117. port => 18083
  118. }]
  119. },
  120. emqx_config:put([emqx_dashboard], Config),
  121. ok;
  122. set_special_configs(emqx_authz) ->
  123. {ok, _} = emqx:update_config([authorization, cache, enable], false),
  124. {ok, _} = emqx:update_config([authorization, no_match], deny),
  125. {ok, _} = emqx:update_config([authorization, sources], []),
  126. ok;
  127. set_special_configs(_App) ->
  128. ok.
  129. init_per_testcase(t_api, Config) ->
  130. meck:new(emqx_misc, [non_strict, passthrough, no_history, no_link]),
  131. meck:expect(emqx_misc, gen_id, fun() -> "fake" end),
  132. meck:new(emqx, [non_strict, passthrough, no_history, no_link]),
  133. meck:expect(emqx, data_dir,
  134. fun() ->
  135. {data_dir, Data} = lists:keyfind(data_dir, 1, Config),
  136. Data
  137. end),
  138. Config;
  139. init_per_testcase(_, Config) -> Config.
  140. end_per_testcase(t_api, _Config) ->
  141. meck:unload(emqx_misc),
  142. meck:unload(emqx),
  143. ok;
  144. end_per_testcase(_, _Config) -> ok.
  145. %%------------------------------------------------------------------------------
  146. %% Testcases
  147. %%------------------------------------------------------------------------------
  148. t_api(_) ->
  149. {ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []),
  150. ?assertEqual([], get_sources(Result1)),
  151. {ok, 204, _} = request(put, uri(["authorization", "sources"]),
  152. [?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]),
  153. {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1),
  154. {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []),
  155. Sources = get_sources(Result2),
  156. ?assertMatch([ #{<<"type">> := <<"http">>}
  157. , #{<<"type">> := <<"mongodb">>}
  158. , #{<<"type">> := <<"mysql">>}
  159. , #{<<"type">> := <<"postgresql">>}
  160. , #{<<"type">> := <<"redis">>}
  161. , #{<<"type">> := <<"file">>}
  162. ], Sources),
  163. ?assert(filelib:is_file(emqx_authz:acl_conf_file())),
  164. {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]),
  165. ?SOURCE1#{<<"enable">> := false}),
  166. {ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []),
  167. ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result3)),
  168. Keyfile = emqx_common_test_helpers:app_path(
  169. emqx,
  170. filename:join(["etc", "certs", "key.pem"])),
  171. Certfile = emqx_common_test_helpers:app_path(
  172. emqx,
  173. filename:join(["etc", "certs", "cert.pem"])),
  174. Cacertfile = emqx_common_test_helpers:app_path(
  175. emqx,
  176. filename:join(["etc", "certs", "cacert.pem"])),
  177. {ok, 204, _} = request(put, uri(["authorization", "sources", "mongodb"]),
  178. ?SOURCE2#{<<"ssl">> => #{
  179. <<"enable">> => <<"true">>,
  180. <<"cacertfile">> => Cacertfile,
  181. <<"certfile">> => Certfile,
  182. <<"keyfile">> => Keyfile,
  183. <<"verify">> => <<"verify_none">>
  184. }}),
  185. {ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongodb"]), []),
  186. ?assertMatch(#{<<"type">> := <<"mongodb">>,
  187. <<"ssl">> := #{<<"enable">> := <<"true">>,
  188. <<"cacertfile">> := ?MATCH_CERT,
  189. <<"certfile">> := ?MATCH_CERT,
  190. <<"keyfile">> := ?MATCH_RSA_KEY,
  191. <<"verify">> := <<"verify_none">>
  192. }
  193. }, jsx:decode(Result4)),
  194. {ok, Cacert} = file:read_file(Cacertfile),
  195. {ok, Cert} = file:read_file(Certfile),
  196. {ok, Key} = file:read_file(Keyfile),
  197. {ok, 204, _} = request(put, uri(["authorization", "sources", "mongodb"]),
  198. ?SOURCE2#{<<"ssl">> => #{
  199. <<"enable">> => <<"true">>,
  200. <<"cacertfile">> => Cacert,
  201. <<"certfile">> => Cert,
  202. <<"keyfile">> => Key,
  203. <<"verify">> => <<"verify_none">>
  204. }}),
  205. {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongodb"]), []),
  206. ?assertMatch(#{<<"type">> := <<"mongodb">>,
  207. <<"ssl">> := #{<<"enable">> := <<"true">>,
  208. <<"cacertfile">> := ?MATCH_CERT,
  209. <<"certfile">> := ?MATCH_CERT,
  210. <<"keyfile">> := ?MATCH_RSA_KEY,
  211. <<"verify">> := <<"verify_none">>
  212. }
  213. }, jsx:decode(Result5)),
  214. #{ssl := #{cacertfile := SavedCacertfile,
  215. certfile := SavedCertfile,
  216. keyfile := SavedKeyfile
  217. }} = emqx_authz:lookup(mongodb),
  218. ?assert(filelib:is_file(SavedCacertfile)),
  219. ?assert(filelib:is_file(SavedCertfile)),
  220. ?assert(filelib:is_file(SavedKeyfile)),
  221. {ok, 204, _} = request(
  222. put,
  223. uri(["authorization", "sources", "mysql"]),
  224. ?SOURCE3#{<<"server">> := <<"192.168.1.100:3306">>}),
  225. {ok, 400, _} = request(
  226. put,
  227. uri(["authorization", "sources", "postgresql"]),
  228. ?SOURCE4#{<<"server">> := <<"fake">>}),
  229. {ok, 400, _} = request(
  230. put,
  231. uri(["authorization", "sources", "redis"]),
  232. ?SOURCE5#{<<"servers">> := [<<"192.168.1.100:6379">>,
  233. <<"192.168.1.100:6380">>]}),
  234. lists:foreach(
  235. fun(#{<<"type">> := Type}) ->
  236. {ok, 204, _} = request(
  237. delete,
  238. uri(["authorization", "sources", binary_to_list(Type)]),
  239. [])
  240. end, Sources),
  241. {ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []),
  242. ?assertEqual([], get_sources(Result6)),
  243. ?assertEqual([], emqx:get_config([authorization, sources])),
  244. ok.
  245. t_move_source(_) ->
  246. {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]),
  247. ?assertMatch([ #{type := http}
  248. , #{type := mongodb}
  249. , #{type := mysql}
  250. , #{type := postgresql}
  251. , #{type := redis}
  252. ], emqx_authz:lookup()),
  253. {ok, 204, _} = request(post, uri(["authorization", "sources", "postgresql", "move"]),
  254. #{<<"position">> => <<"top">>}),
  255. ?assertMatch([ #{type := postgresql}
  256. , #{type := http}
  257. , #{type := mongodb}
  258. , #{type := mysql}
  259. , #{type := redis}
  260. ], emqx_authz:lookup()),
  261. {ok, 204, _} = request(post, uri(["authorization", "sources", "http", "move"]),
  262. #{<<"position">> => <<"bottom">>}),
  263. ?assertMatch([ #{type := postgresql}
  264. , #{type := mongodb}
  265. , #{type := mysql}
  266. , #{type := redis}
  267. , #{type := http}
  268. ], emqx_authz:lookup()),
  269. {ok, 204, _} = request(post, uri(["authorization", "sources", "mysql", "move"]),
  270. #{<<"position">> => #{<<"before">> => <<"postgresql">>}}),
  271. ?assertMatch([ #{type := mysql}
  272. , #{type := postgresql}
  273. , #{type := mongodb}
  274. , #{type := redis}
  275. , #{type := http}
  276. ], emqx_authz:lookup()),
  277. {ok, 204, _} = request(post, uri(["authorization", "sources", "mongodb", "move"]),
  278. #{<<"position">> => #{<<"after">> => <<"http">>}}),
  279. ?assertMatch([ #{type := mysql}
  280. , #{type := postgresql}
  281. , #{type := redis}
  282. , #{type := http}
  283. , #{type := mongodb}
  284. ], emqx_authz:lookup()),
  285. ok.
  286. %%--------------------------------------------------------------------
  287. %% HTTP Request
  288. %%--------------------------------------------------------------------
  289. request(Method, Url, Body) ->
  290. Request = case Body of
  291. [] -> {Url, [auth_header_()]};
  292. _ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)}
  293. end,
  294. ct:pal("Method: ~p, Request: ~p", [Method, Request]),
  295. case httpc:request(Method, Request, [], [{body_format, binary}]) of
  296. {error, socket_closed_remotely} ->
  297. {error, socket_closed_remotely};
  298. {ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } ->
  299. {ok, Code, Return};
  300. {ok, {Reason, _, _}} ->
  301. {error, Reason}
  302. end.
  303. uri() -> uri([]).
  304. uri(Parts) when is_list(Parts) ->
  305. NParts = [E || E <- Parts],
  306. ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
  307. get_sources(Result) ->
  308. maps:get(<<"sources">>, jsx:decode(Result), []).
  309. auth_header_() ->
  310. Username = <<"admin">>,
  311. Password = <<"public">>,
  312. {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
  313. {"Authorization", "Bearer " ++ binary_to_list(Token)}.
  314. data_dir() -> emqx:data_dir().