emqx_authz_api_mnesia.erl 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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_authz_api_mnesia).
  17. -behaviour(minirest_api).
  18. -include("emqx_authz.hrl").
  19. -include_lib("emqx/include/logger.hrl").
  20. -include_lib("typerefl/include/types.hrl").
  21. -define(FORMAT_USERNAME_FUN, {?MODULE, format_by_username}).
  22. -define(FORMAT_CLIENTID_FUN, {?MODULE, format_by_clientid}).
  23. -export([ api_spec/0
  24. , paths/0
  25. , schema/1
  26. , fields/1
  27. ]).
  28. %% operation funs
  29. -export([ users/2
  30. , clients/2
  31. , user/2
  32. , client/2
  33. , all/2
  34. , purge/2
  35. ]).
  36. -export([ format_by_username/1
  37. , format_by_clientid/1]).
  38. -define(BAD_REQUEST, 'BAD_REQUEST').
  39. -define(NOT_FOUND, 'NOT_FOUND').
  40. -define(TYPE_REF, ref).
  41. -define(TYPE_ARRAY, array).
  42. -define(PAGE_QUERY_EXAMPLE, example_in_data).
  43. -define(PUT_MAP_EXAMPLE, in_put_requestBody).
  44. -define(POST_ARRAY_EXAMPLE, in_post_requestBody).
  45. api_spec() ->
  46. emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
  47. paths() ->
  48. [ "/authorization/sources/built-in-database/username"
  49. , "/authorization/sources/built-in-database/clientid"
  50. , "/authorization/sources/built-in-database/username/:username"
  51. , "/authorization/sources/built-in-database/clientid/:clientid"
  52. , "/authorization/sources/built-in-database/all"
  53. , "/authorization/sources/built-in-database/purge-all"].
  54. %%--------------------------------------------------------------------
  55. %% Schema for each URI
  56. %%--------------------------------------------------------------------
  57. schema("/authorization/sources/built-in-database/username") ->
  58. #{
  59. 'operationId' => users,
  60. get => #{
  61. tags => [<<"authorization">>],
  62. description => <<"Show the list of record for username">>,
  63. parameters => [ hoconsc:ref(emqx_dashboard_swagger, page)
  64. , hoconsc:ref(emqx_dashboard_swagger, limit)],
  65. responses => #{
  66. 200 => swagger_with_example( {username_response_data, ?TYPE_REF}
  67. , {username, ?PAGE_QUERY_EXAMPLE})
  68. }
  69. },
  70. post => #{
  71. tags => [<<"authorization">>],
  72. description => <<"Add new records for username">>,
  73. 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_ARRAY}
  74. , {username, ?POST_ARRAY_EXAMPLE}),
  75. responses => #{
  76. 204 => <<"Created">>,
  77. 400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST]
  78. , <<"Bad username or bad rule schema">>)
  79. }
  80. }
  81. };
  82. schema("/authorization/sources/built-in-database/clientid") ->
  83. #{
  84. 'operationId' => clients,
  85. get => #{
  86. tags => [<<"authorization">>],
  87. description => <<"Show the list of record for clientid">>,
  88. parameters => [ hoconsc:ref(emqx_dashboard_swagger, page)
  89. , hoconsc:ref(emqx_dashboard_swagger, limit)],
  90. responses => #{
  91. 200 => swagger_with_example( {clientid_response_data, ?TYPE_REF}
  92. , {clientid, ?PAGE_QUERY_EXAMPLE})
  93. }
  94. },
  95. post => #{
  96. tags => [<<"authorization">>],
  97. description => <<"Add new records for clientid">>,
  98. 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_ARRAY}
  99. , {clientid, ?POST_ARRAY_EXAMPLE}),
  100. responses => #{
  101. 204 => <<"Created">>,
  102. 400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST]
  103. , <<"Bad clientid or bad rule schema">>)
  104. }
  105. }
  106. };
  107. schema("/authorization/sources/built-in-database/username/:username") ->
  108. #{
  109. 'operationId' => user,
  110. get => #{
  111. tags => [<<"authorization">>],
  112. description => <<"Get record info for username">>,
  113. parameters => [hoconsc:ref(username)],
  114. responses => #{
  115. 200 => swagger_with_example( {rules_for_username, ?TYPE_REF}
  116. , {username, ?PUT_MAP_EXAMPLE}),
  117. 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
  118. }
  119. },
  120. put => #{
  121. tags => [<<"authorization">>],
  122. description => <<"Set record for username">>,
  123. parameters => [hoconsc:ref(username)],
  124. 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_REF}
  125. , {username, ?PUT_MAP_EXAMPLE}),
  126. responses => #{
  127. 204 => <<"Updated">>,
  128. 400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST]
  129. , <<"Bad username or bad rule schema">>)
  130. }
  131. },
  132. delete => #{
  133. tags => [<<"authorization">>],
  134. description => <<"Delete one record for username">>,
  135. parameters => [hoconsc:ref(username)],
  136. responses => #{
  137. 204 => <<"Deleted">>,
  138. 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad username">>)
  139. }
  140. }
  141. };
  142. schema("/authorization/sources/built-in-database/clientid/:clientid") ->
  143. #{
  144. 'operationId' => client,
  145. get => #{
  146. tags => [<<"authorization">>],
  147. description => <<"Get record info for clientid">>,
  148. parameters => [hoconsc:ref(clientid)],
  149. responses => #{
  150. 200 => swagger_with_example( {rules_for_clientid, ?TYPE_REF}
  151. , {clientid, ?PUT_MAP_EXAMPLE}),
  152. 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
  153. }
  154. },
  155. put => #{
  156. tags => [<<"authorization">>],
  157. description => <<"Set record for clientid">>,
  158. parameters => [hoconsc:ref(clientid)],
  159. 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_REF}
  160. , {clientid, ?PUT_MAP_EXAMPLE}),
  161. responses => #{
  162. 204 => <<"Updated">>,
  163. 400 => emqx_dashboard_swagger:error_codes(
  164. [?BAD_REQUEST], <<"Bad clientid or bad rule schema">>)
  165. }
  166. },
  167. delete => #{
  168. tags => [<<"authorization">>],
  169. description => <<"Delete one record for clientid">>,
  170. parameters => [hoconsc:ref(clientid)],
  171. responses => #{
  172. 204 => <<"Deleted">>,
  173. 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad clientid">>)
  174. }
  175. }
  176. };
  177. schema("/authorization/sources/built-in-database/all") ->
  178. #{
  179. 'operationId' => all,
  180. get => #{
  181. tags => [<<"authorization">>],
  182. description => <<"Show the list of rules for all">>,
  183. responses => #{
  184. 200 => swagger_with_example({rules_for_all, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})
  185. }
  186. },
  187. put => #{
  188. tags => [<<"authorization">>],
  189. description => <<"Set the list of rules for all">>,
  190. 'requestBody' =>
  191. swagger_with_example({rules_for_all, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}),
  192. responses => #{
  193. 204 => <<"Created">>,
  194. 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad rule schema">>)
  195. }
  196. }
  197. };
  198. schema("/authorization/sources/built-in-database/purge-all") ->
  199. #{
  200. 'operationId' => purge,
  201. delete => #{
  202. tags => [<<"authorization">>],
  203. description => <<"Purge all records">>,
  204. responses => #{
  205. 204 => <<"Deleted">>,
  206. 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
  207. }
  208. }
  209. }.
  210. fields(rule_item) ->
  211. [ {topic, hoconsc:mk(string(),
  212. #{ required => true
  213. , desc => <<"Rule on specific topic">>
  214. , example => <<"test/topic/1">>
  215. })}
  216. , {permission, hoconsc:mk(hoconsc:enum([allow, deny]),
  217. #{ desc => <<"Permission">>
  218. , required => true
  219. , example => allow
  220. })}
  221. , {action, hoconsc:mk(hoconsc:enum([publish, subscribe, all]),
  222. #{ required => true
  223. , example => publish
  224. , desc => <<"Authorized action">>
  225. })}
  226. ];
  227. fields(clientid) ->
  228. [ {clientid, hoconsc:mk(binary(),
  229. #{ in => path
  230. , required => true
  231. , desc => <<"ClientID">>
  232. , example => <<"client1">>
  233. })}
  234. ];
  235. fields(username) ->
  236. [ {username, hoconsc:mk(binary(),
  237. #{ in => path
  238. , required => true
  239. , desc => <<"Username">>
  240. , example => <<"user1">>})}
  241. ];
  242. fields(rules_for_username) ->
  243. [ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})}
  244. ] ++ fields(username);
  245. fields(username_response_data) ->
  246. [ {data, hoconsc:mk(hoconsc:array(hoconsc:ref(rules_for_username)), #{})}
  247. , {meta, hoconsc:ref(meta)}
  248. ];
  249. fields(rules_for_clientid) ->
  250. [ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})}
  251. ] ++ fields(clientid);
  252. fields(clientid_response_data) ->
  253. [ {data, hoconsc:mk(hoconsc:array(hoconsc:ref(rules_for_clientid)), #{})}
  254. , {meta, hoconsc:ref(meta)}
  255. ];
  256. fields(rules_for_all) ->
  257. [ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})}
  258. ];
  259. fields(meta) ->
  260. emqx_dashboard_swagger:fields(page)
  261. ++ emqx_dashboard_swagger:fields(limit)
  262. ++ [{count, hoconsc:mk(integer(), #{example => 1})}].
  263. %%--------------------------------------------------------------------
  264. %% HTTP API
  265. %%--------------------------------------------------------------------
  266. users(get, #{query_string := PageParams}) ->
  267. {Table, MatchSpec} = emqx_authz_mnesia:list_username_rules(),
  268. {200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?FORMAT_USERNAME_FUN)};
  269. users(post, #{body := Body}) when is_list(Body) ->
  270. lists:foreach(fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
  271. emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules))
  272. end, Body),
  273. {204}.
  274. clients(get, #{query_string := PageParams}) ->
  275. {Table, MatchSpec} = emqx_authz_mnesia:list_clientid_rules(),
  276. {200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?FORMAT_CLIENTID_FUN)};
  277. clients(post, #{body := Body}) when is_list(Body) ->
  278. lists:foreach(fun(#{<<"clientid">> := Clientid, <<"rules">> := Rules}) ->
  279. emqx_authz_mnesia:store_rules({clientid, Clientid}, format_rules(Rules))
  280. end, Body),
  281. {204}.
  282. user(get, #{bindings := #{username := Username}}) ->
  283. case emqx_authz_mnesia:get_rules({username, Username}) of
  284. not_found -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
  285. {ok, Rules} ->
  286. {200, #{username => Username,
  287. rules => [ #{topic => Topic,
  288. action => Action,
  289. permission => Permission
  290. } || {Permission, Action, Topic} <- Rules]}
  291. }
  292. end;
  293. user(put, #{bindings := #{username := Username},
  294. body := #{<<"username">> := Username, <<"rules">> := Rules}}) ->
  295. emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)),
  296. {204};
  297. user(delete, #{bindings := #{username := Username}}) ->
  298. emqx_authz_mnesia:delete_rules({username, Username}),
  299. {204}.
  300. client(get, #{bindings := #{clientid := Clientid}}) ->
  301. case emqx_authz_mnesia:get_rules({clientid, Clientid}) of
  302. not_found -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
  303. {ok, Rules} ->
  304. {200, #{clientid => Clientid,
  305. rules => [ #{topic => Topic,
  306. action => Action,
  307. permission => Permission
  308. } || {Permission, Action, Topic} <- Rules]}
  309. }
  310. end;
  311. client(put, #{bindings := #{clientid := Clientid},
  312. body := #{<<"clientid">> := Clientid, <<"rules">> := Rules}}) ->
  313. emqx_authz_mnesia:store_rules({clientid, Clientid}, format_rules(Rules)),
  314. {204};
  315. client(delete, #{bindings := #{clientid := Clientid}}) ->
  316. emqx_authz_mnesia:delete_rules({clientid, Clientid}),
  317. {204}.
  318. all(get, _) ->
  319. case emqx_authz_mnesia:get_rules(all) of
  320. not_found ->
  321. {200, #{rules => []}};
  322. {ok, Rules} ->
  323. {200, #{rules => [ #{topic => Topic,
  324. action => Action,
  325. permission => Permission
  326. } || {Permission, Action, Topic} <- Rules]}
  327. }
  328. end;
  329. all(put, #{body := #{<<"rules">> := Rules}}) ->
  330. emqx_authz_mnesia:store_rules(all, format_rules(Rules)),
  331. {204}.
  332. purge(delete, _) ->
  333. case emqx_authz_api_sources:get_raw_source(<<"built-in-database">>) of
  334. [#{<<"enable">> := false}] ->
  335. ok = emqx_authz_mnesia:purge_rules(),
  336. {204};
  337. [#{<<"enable">> := true}] ->
  338. {400, #{code => <<"BAD_REQUEST">>,
  339. message =>
  340. <<"'built-in-database' type source must be disabled before purge.">>}};
  341. [] ->
  342. {404, #{code => <<"BAD_REQUEST">>,
  343. message => <<"'built-in-database' type source is not found.">>
  344. }}
  345. end.
  346. format_rules(Rules) when is_list(Rules) ->
  347. lists:foldl(fun(#{<<"topic">> := Topic,
  348. <<"action">> := Action,
  349. <<"permission">> := Permission
  350. }, AccIn) when ?PUBSUB(Action)
  351. andalso ?ALLOW_DENY(Permission) ->
  352. AccIn ++ [{ atom(Permission), atom(Action), Topic }]
  353. end, [], Rules).
  354. format_by_username([{username, Username}, {rules, Rules}]) ->
  355. #{username => Username,
  356. rules => [ #{topic => Topic,
  357. action => Action,
  358. permission => Permission
  359. } || {Permission, Action, Topic} <- Rules]
  360. }.
  361. format_by_clientid([{clientid, Clientid}, {rules, Rules}]) ->
  362. #{clientid => Clientid,
  363. rules => [ #{topic => Topic,
  364. action => Action,
  365. permission => Permission
  366. } || {Permission, Action, Topic} <- Rules]
  367. }.
  368. atom(B) when is_binary(B) ->
  369. try binary_to_existing_atom(B, utf8)
  370. catch
  371. _Error:_Expection -> binary_to_atom(B)
  372. end;
  373. atom(A) when is_atom(A) -> A.
  374. %%--------------------------------------------------------------------
  375. %% Internal functions
  376. %%--------------------------------------------------------------------
  377. swagger_with_example({Ref, TypeP}, {_Name, _Type} = Example) ->
  378. emqx_dashboard_swagger:schema_with_examples(
  379. case TypeP of
  380. ?TYPE_REF -> hoconsc:ref(?MODULE, Ref);
  381. ?TYPE_ARRAY -> hoconsc:array(hoconsc:ref(?MODULE, Ref))
  382. end,
  383. rules_example(Example)).
  384. rules_example({ExampleName, ExampleType}) ->
  385. {Summary, Example} =
  386. case ExampleName of
  387. username -> {<<"Username">>, ?USERNAME_RULES_EXAMPLE};
  388. clientid -> {<<"ClientID">>, ?CLIENTID_RULES_EXAMPLE};
  389. all -> {<<"All">>, ?ALL_RULES_EXAMPLE}
  390. end,
  391. Value =
  392. case ExampleType of
  393. ?PAGE_QUERY_EXAMPLE -> #{
  394. data => [Example],
  395. meta => ?META_EXAMPLE
  396. };
  397. ?PUT_MAP_EXAMPLE ->
  398. Example;
  399. ?POST_ARRAY_EXAMPLE ->
  400. [Example]
  401. end,
  402. #{
  403. 'password-based:built-in-database' => #{
  404. summary => Summary,
  405. value => Value
  406. }
  407. }.