emqx_authz_api_sources.erl 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-2023 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_sources).
  17. -behaviour(minirest_api).
  18. -include("emqx_authz.hrl").
  19. -include_lib("emqx/include/logger.hrl").
  20. -include_lib("hocon/include/hoconsc.hrl").
  21. -import(hoconsc, [mk/1, mk/2, ref/2, array/1, enum/1]).
  22. -define(BAD_REQUEST, 'BAD_REQUEST').
  23. -define(NOT_FOUND, 'NOT_FOUND').
  24. -define(API_SCHEMA_MODULE, emqx_authz_api_schema).
  25. -export([
  26. get_raw_sources/0,
  27. get_raw_source/1,
  28. source_status/2,
  29. lookup_from_local_node/1,
  30. lookup_from_all_nodes/1
  31. ]).
  32. -export([
  33. api_spec/0,
  34. paths/0,
  35. schema/1,
  36. fields/1
  37. ]).
  38. -export([
  39. sources/2,
  40. source/2,
  41. source_move/2,
  42. aggregate_metrics/1
  43. ]).
  44. -define(TAGS, [<<"Authorization">>]).
  45. api_spec() ->
  46. emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
  47. paths() ->
  48. [
  49. "/authorization/sources",
  50. "/authorization/sources/:type",
  51. "/authorization/sources/:type/status",
  52. "/authorization/sources/:type/move"
  53. ].
  54. fields(sources) ->
  55. [{sources, mk(array(hoconsc:union(authz_sources_type_refs())), #{desc => ?DESC(sources)})}].
  56. %%--------------------------------------------------------------------
  57. %% Schema for each URI
  58. %%--------------------------------------------------------------------
  59. schema("/authorization/sources") ->
  60. #{
  61. 'operationId' => sources,
  62. get =>
  63. #{
  64. description => ?DESC(authorization_sources_get),
  65. tags => ?TAGS,
  66. responses =>
  67. #{
  68. 200 => ref(?MODULE, sources)
  69. }
  70. },
  71. post =>
  72. #{
  73. description => ?DESC(authorization_sources_post),
  74. tags => ?TAGS,
  75. 'requestBody' => mk(
  76. hoconsc:union(authz_sources_type_refs()),
  77. #{desc => ?DESC(source_config)}
  78. ),
  79. responses =>
  80. #{
  81. 204 => <<"Authorization source created successfully">>,
  82. 400 => emqx_dashboard_swagger:error_codes(
  83. [?BAD_REQUEST],
  84. <<"Bad Request">>
  85. )
  86. }
  87. }
  88. };
  89. schema("/authorization/sources/:type") ->
  90. #{
  91. 'operationId' => source,
  92. get =>
  93. #{
  94. description => ?DESC(authorization_sources_type_get),
  95. tags => ?TAGS,
  96. parameters => parameters_field(),
  97. responses =>
  98. #{
  99. 200 => mk(
  100. hoconsc:union(authz_sources_type_refs()),
  101. #{desc => ?DESC(source)}
  102. ),
  103. 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
  104. }
  105. },
  106. put =>
  107. #{
  108. description => ?DESC(authorization_sources_type_put),
  109. tags => ?TAGS,
  110. parameters => parameters_field(),
  111. 'requestBody' => mk(hoconsc:union(authz_sources_type_refs())),
  112. responses =>
  113. #{
  114. 204 => <<"Authorization source updated successfully">>,
  115. 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
  116. }
  117. },
  118. delete =>
  119. #{
  120. description => ?DESC(authorization_sources_type_delete),
  121. tags => ?TAGS,
  122. parameters => parameters_field(),
  123. responses =>
  124. #{
  125. 204 => <<"Deleted successfully">>,
  126. 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
  127. }
  128. }
  129. };
  130. schema("/authorization/sources/:type/status") ->
  131. #{
  132. 'operationId' => source_status,
  133. get =>
  134. #{
  135. description => ?DESC(authorization_sources_type_status_get),
  136. tags => ?TAGS,
  137. parameters => parameters_field(),
  138. responses =>
  139. #{
  140. 200 => emqx_dashboard_swagger:schema_with_examples(
  141. hoconsc:ref(emqx_authz_schema, "metrics_status_fields"),
  142. status_metrics_example()
  143. ),
  144. 400 => emqx_dashboard_swagger:error_codes(
  145. [?BAD_REQUEST], <<"Bad request">>
  146. ),
  147. 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
  148. }
  149. }
  150. };
  151. schema("/authorization/sources/:type/move") ->
  152. #{
  153. 'operationId' => source_move,
  154. post =>
  155. #{
  156. description => ?DESC(authorization_sources_type_move_post),
  157. tags => ?TAGS,
  158. parameters => parameters_field(),
  159. 'requestBody' =>
  160. emqx_dashboard_swagger:schema_with_examples(
  161. ref(?API_SCHEMA_MODULE, position),
  162. position_example()
  163. ),
  164. responses =>
  165. #{
  166. 204 => <<"No Content">>,
  167. 400 => emqx_dashboard_swagger:error_codes(
  168. [?BAD_REQUEST], <<"Bad Request">>
  169. ),
  170. 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>)
  171. }
  172. }
  173. }.
  174. %%--------------------------------------------------------------------
  175. %% Operation functions
  176. %%--------------------------------------------------------------------
  177. sources(Method, #{bindings := #{type := Type} = Bindings} = Req) when
  178. is_atom(Type)
  179. ->
  180. sources(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
  181. sources(get, _) ->
  182. Sources = lists:foldl(
  183. fun
  184. (
  185. #{
  186. <<"type">> := <<"file">>,
  187. <<"enable">> := Enable,
  188. <<"path">> := Path
  189. },
  190. AccIn
  191. ) ->
  192. case file:read_file(Path) of
  193. {ok, Rules} ->
  194. lists:append(AccIn, [
  195. #{
  196. type => file,
  197. enable => Enable,
  198. rules => Rules
  199. }
  200. ]);
  201. {error, _} ->
  202. lists:append(AccIn, [
  203. #{
  204. type => file,
  205. enable => Enable,
  206. rules => <<"">>
  207. }
  208. ])
  209. end;
  210. (Source, AccIn) ->
  211. lists:append(AccIn, [Source])
  212. end,
  213. [],
  214. get_raw_sources()
  215. ),
  216. {200, #{sources => Sources}};
  217. sources(post, #{body := Body}) ->
  218. update_config(?CMD_PREPEND, Body).
  219. source(Method, #{bindings := #{type := Type} = Bindings} = Req) when
  220. is_atom(Type)
  221. ->
  222. source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
  223. source(get, #{bindings := #{type := Type}}) ->
  224. with_source(
  225. Type,
  226. fun
  227. (#{<<"type">> := <<"file">>, <<"enable">> := Enable, <<"path">> := Path}) ->
  228. case file:read_file(Path) of
  229. {ok, Rules} ->
  230. {200, #{
  231. type => file,
  232. enable => Enable,
  233. rules => Rules
  234. }};
  235. {error, Reason} ->
  236. {500, #{
  237. code => <<"INTERNAL_ERROR">>,
  238. message => bin(Reason)
  239. }}
  240. end;
  241. (Source) ->
  242. {200, Source}
  243. end
  244. );
  245. source(put, #{bindings := #{type := Type}, body := #{<<"type">> := Type} = Body}) ->
  246. with_source(
  247. Type,
  248. fun(_) ->
  249. update_config({?CMD_REPLACE, Type}, Body)
  250. end
  251. );
  252. source(put, #{bindings := #{type := Type}, body := #{<<"type">> := _OtherType}}) ->
  253. with_source(
  254. Type,
  255. fun(_) ->
  256. {400, #{code => <<"BAD_REQUEST">>, message => <<"Type mismatch">>}}
  257. end
  258. );
  259. source(delete, #{bindings := #{type := Type}}) ->
  260. with_source(
  261. Type,
  262. fun(_) ->
  263. update_config({?CMD_DELETE, Type}, #{})
  264. end
  265. ).
  266. source_status(get, #{bindings := #{type := Type}}) ->
  267. with_source(
  268. atom_to_binary(Type, utf8),
  269. fun(_) -> lookup_from_all_nodes(Type) end
  270. ).
  271. source_move(Method, #{bindings := #{type := Type} = Bindings} = Req) when
  272. is_atom(Type)
  273. ->
  274. source_move(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
  275. source_move(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) ->
  276. with_source(
  277. Type,
  278. fun(_Source) ->
  279. case parse_position(Position) of
  280. {ok, NPosition} ->
  281. try emqx_authz:move(Type, NPosition) of
  282. {ok, _} ->
  283. {204};
  284. {error, {not_found_source, _Type}} ->
  285. {404, #{
  286. code => <<"NOT_FOUND">>,
  287. message => <<"source ", Type/binary, " not found">>
  288. }};
  289. {error, {emqx_conf_schema, _}} ->
  290. {400, #{
  291. code => <<"BAD_REQUEST">>,
  292. message => <<"BAD_SCHEMA">>
  293. }};
  294. {error, Reason} ->
  295. {400, #{
  296. code => <<"BAD_REQUEST">>,
  297. message => bin(Reason)
  298. }}
  299. catch
  300. error:{unknown_authz_source_type, Unknown} ->
  301. NUnknown = bin(Unknown),
  302. {400, #{
  303. code => <<"BAD_REQUEST">>,
  304. message => <<"Unknown authz Source Type: ", NUnknown/binary>>
  305. }}
  306. end;
  307. {error, Reason} ->
  308. {400, #{
  309. code => <<"BAD_REQUEST">>,
  310. message => bin(Reason)
  311. }}
  312. end
  313. end
  314. ).
  315. %%--------------------------------------------------------------------
  316. %% Internal functions
  317. %%--------------------------------------------------------------------
  318. lookup_from_local_node(Type) ->
  319. NodeId = node(self()),
  320. try emqx_authz:lookup(Type) of
  321. #{annotations := #{id := ResourceId}} ->
  322. Metrics = emqx_metrics_worker:get_metrics(authz_metrics, Type),
  323. case emqx_resource:get_instance(ResourceId) of
  324. {error, not_found} ->
  325. {error, {NodeId, not_found_resource}};
  326. {ok, _, #{status := Status}} ->
  327. {ok, {NodeId, Status, Metrics, emqx_resource:get_metrics(ResourceId)}}
  328. end;
  329. _ ->
  330. Metrics = emqx_metrics_worker:get_metrics(authz_metrics, Type),
  331. %% for authz file/authz mnesia
  332. {ok, {NodeId, connected, Metrics, #{}}}
  333. catch
  334. _:Reason -> {error, {NodeId, list_to_binary(io_lib:format("~p", [Reason]))}}
  335. end.
  336. lookup_from_all_nodes(Type) ->
  337. Nodes = mria:running_nodes(),
  338. case is_ok(emqx_authz_proto_v1:lookup_from_all_nodes(Nodes, Type)) of
  339. {ok, ResList} ->
  340. {StatusMap, MetricsMap, ResourceMetricsMap, ErrorMap} = make_result_map(ResList),
  341. AggregateStatus = aggregate_status(maps:values(StatusMap)),
  342. AggregateMetrics = aggregate_metrics(maps:values(MetricsMap)),
  343. AggregateResourceMetrics = aggregate_metrics(maps:values(ResourceMetricsMap)),
  344. Fun = fun(_, V1) -> restructure_map(V1) end,
  345. MKMap = fun(Name) -> fun({Key, Val}) -> #{node => Key, Name => Val} end end,
  346. HelpFun = fun(M, Name) -> lists:map(MKMap(Name), maps:to_list(M)) end,
  347. {200, #{
  348. node_resource_metrics => HelpFun(maps:map(Fun, ResourceMetricsMap), metrics),
  349. resource_metrics =>
  350. case maps:size(AggregateResourceMetrics) of
  351. 0 -> #{};
  352. _ -> restructure_map(AggregateResourceMetrics)
  353. end,
  354. node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
  355. metrics => restructure_map(AggregateMetrics),
  356. node_status => HelpFun(StatusMap, status),
  357. status => AggregateStatus,
  358. node_error => HelpFun(maps:map(Fun, ErrorMap), reason)
  359. }};
  360. {error, ErrL} ->
  361. {400, #{
  362. code => <<"INTERNAL_ERROR">>,
  363. message => bin_t(io_lib:format("~p", [ErrL]))
  364. }}
  365. end.
  366. aggregate_status([]) ->
  367. empty_metrics_and_status;
  368. aggregate_status(AllStatus) ->
  369. Head = fun([A | _]) -> A end,
  370. HeadVal = Head(AllStatus),
  371. AllRes = lists:all(fun(Val) -> Val == HeadVal end, AllStatus),
  372. case AllRes of
  373. true -> HeadVal;
  374. false -> inconsistent
  375. end.
  376. aggregate_metrics([]) ->
  377. #{};
  378. aggregate_metrics([HeadMetrics | AllMetrics]) ->
  379. ErrorLogger = fun(Reason) -> ?SLOG(info, #{msg => "bad_metrics_value", error => Reason}) end,
  380. Fun = fun(ElemMap, AccMap) ->
  381. emqx_utils_maps:best_effort_recursive_sum(AccMap, ElemMap, ErrorLogger)
  382. end,
  383. lists:foldl(Fun, HeadMetrics, AllMetrics).
  384. make_result_map(ResList) ->
  385. Fun =
  386. fun(Elem, {StatusMap, MetricsMap, ResourceMetricsMap, ErrorMap}) ->
  387. case Elem of
  388. {ok, {NodeId, Status, Metrics, ResourceMetrics}} ->
  389. {
  390. maps:put(NodeId, Status, StatusMap),
  391. maps:put(NodeId, Metrics, MetricsMap),
  392. maps:put(NodeId, ResourceMetrics, ResourceMetricsMap),
  393. ErrorMap
  394. };
  395. {error, {NodeId, Reason}} ->
  396. {StatusMap, MetricsMap, ResourceMetricsMap, maps:put(NodeId, Reason, ErrorMap)}
  397. end
  398. end,
  399. lists:foldl(Fun, {maps:new(), maps:new(), maps:new(), maps:new()}, ResList).
  400. restructure_map(#{
  401. counters := #{deny := Failed, total := Total, allow := Succ, nomatch := Nomatch},
  402. rate := #{total := #{current := Rate, last5m := Rate5m, max := RateMax}}
  403. }) ->
  404. #{
  405. total => Total,
  406. allow => Succ,
  407. deny => Failed,
  408. nomatch => Nomatch,
  409. rate => Rate,
  410. rate_last5m => Rate5m,
  411. rate_max => RateMax
  412. };
  413. restructure_map(#{
  414. counters := #{failed := Failed, matched := Match, success := Succ},
  415. rate := #{matched := #{current := Rate, last5m := Rate5m, max := RateMax}}
  416. }) ->
  417. #{
  418. matched => Match,
  419. success => Succ,
  420. failed => Failed,
  421. rate => Rate,
  422. rate_last5m => Rate5m,
  423. rate_max => RateMax
  424. };
  425. restructure_map(Error) ->
  426. Error.
  427. bin_t(S) when is_list(S) ->
  428. list_to_binary(S).
  429. is_ok(ResL) ->
  430. case
  431. lists:filter(
  432. fun
  433. ({ok, _}) -> false;
  434. (_) -> true
  435. end,
  436. ResL
  437. )
  438. of
  439. [] -> {ok, [Res || {ok, Res} <- ResL]};
  440. ErrL -> {error, ErrL}
  441. end.
  442. get_raw_sources() ->
  443. RawSources = emqx:get_raw_config([authorization, sources], []),
  444. Schema = emqx_hocon:make_schema(emqx_authz_schema:authz_fields()),
  445. Conf = #{<<"sources">> => RawSources},
  446. #{<<"sources">> := Sources} = hocon_tconf:make_serializable(Schema, Conf, #{}),
  447. merge_default_headers(Sources).
  448. merge_default_headers(Sources) ->
  449. lists:map(
  450. fun(Source) ->
  451. case maps:find(<<"headers">>, Source) of
  452. {ok, Headers} ->
  453. NewHeaders =
  454. case Source of
  455. #{<<"method">> := <<"get">>} ->
  456. (emqx_authz_schema:headers_no_content_type(converter))(Headers);
  457. #{<<"method">> := <<"post">>} ->
  458. (emqx_authz_schema:headers(converter))(Headers);
  459. _ ->
  460. Headers
  461. end,
  462. Source#{<<"headers">> => NewHeaders};
  463. error ->
  464. Source
  465. end
  466. end,
  467. Sources
  468. ).
  469. get_raw_source(Type) ->
  470. lists:filter(
  471. fun(#{<<"type">> := T}) ->
  472. T =:= Type
  473. end,
  474. get_raw_sources()
  475. ).
  476. -spec with_source(binary(), fun((map()) -> term())) -> term().
  477. with_source(Type, ContF) ->
  478. case get_raw_source(Type) of
  479. [] ->
  480. {404, #{code => <<"NOT_FOUND">>, message => <<"Not found: ", Type/binary>>}};
  481. [Source] ->
  482. ContF(Source)
  483. end.
  484. update_config(Cmd, Sources) ->
  485. case emqx_authz:update(Cmd, Sources) of
  486. {ok, _} ->
  487. {204};
  488. {error, {pre_config_update, emqx_authz, Reason}} ->
  489. {400, #{
  490. code => <<"BAD_REQUEST">>,
  491. message => bin(Reason)
  492. }};
  493. {error, {post_config_update, emqx_authz, Reason}} ->
  494. {400, #{
  495. code => <<"BAD_REQUEST">>,
  496. message => bin(Reason)
  497. }};
  498. %% TODO: The `Reason` may cann't be trans to json term. (i.e. ecpool start failed)
  499. {error, {emqx_conf_schema, _}} ->
  500. {400, #{
  501. code => <<"BAD_REQUEST">>,
  502. message => <<"BAD_SCHEMA">>
  503. }};
  504. {error, Reason} ->
  505. {400, #{
  506. code => <<"BAD_REQUEST">>,
  507. message => bin(Reason)
  508. }}
  509. end.
  510. parameters_field() ->
  511. [
  512. {type,
  513. mk(
  514. enum(?API_SCHEMA_MODULE:authz_sources_types(simple)),
  515. #{in => path, desc => ?DESC(source_type)}
  516. )}
  517. ].
  518. parse_position(<<"front">>) ->
  519. {ok, ?CMD_MOVE_FRONT};
  520. parse_position(<<"rear">>) ->
  521. {ok, ?CMD_MOVE_REAR};
  522. parse_position(<<"before:">>) ->
  523. {error, <<"Invalid parameter. Cannot be placed before an empty target">>};
  524. parse_position(<<"after:">>) ->
  525. {error, <<"Invalid parameter. Cannot be placed after an empty target">>};
  526. parse_position(<<"before:", Before/binary>>) ->
  527. {ok, ?CMD_MOVE_BEFORE(Before)};
  528. parse_position(<<"after:", After/binary>>) ->
  529. {ok, ?CMD_MOVE_AFTER(After)};
  530. parse_position(_) ->
  531. {error, <<"Invalid parameter. Unknow position">>}.
  532. position_example() ->
  533. #{
  534. front =>
  535. #{
  536. summary => <<"front example">>,
  537. value => #{<<"position">> => <<"front">>}
  538. },
  539. rear =>
  540. #{
  541. summary => <<"rear example">>,
  542. value => #{<<"position">> => <<"rear">>}
  543. },
  544. relative_before =>
  545. #{
  546. summary => <<"relative example">>,
  547. value => #{<<"position">> => <<"before:file">>}
  548. },
  549. relative_after =>
  550. #{
  551. summary => <<"relative example">>,
  552. value => #{<<"position">> => <<"after:file">>}
  553. }
  554. }.
  555. authz_sources_type_refs() ->
  556. [
  557. ref(?API_SCHEMA_MODULE, Type)
  558. || Type <- emqx_authz_api_schema:authz_sources_types(detailed)
  559. ].
  560. bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])).
  561. status_metrics_example() ->
  562. #{
  563. 'metrics_example' => #{
  564. summary => <<"Showing a typical metrics example">>,
  565. value =>
  566. #{
  567. resource_metrics => #{
  568. matched => 0,
  569. success => 0,
  570. failed => 0,
  571. rate => 0.0,
  572. rate_last5m => 0.0,
  573. rate_max => 0.0
  574. },
  575. node_resource_metrics => [
  576. #{
  577. node => node(),
  578. metrics => #{
  579. matched => 0,
  580. success => 0,
  581. failed => 0,
  582. rate => 0.0,
  583. rate_last5m => 0.0,
  584. rate_max => 0.0
  585. }
  586. }
  587. ],
  588. metrics => #{
  589. total => 0,
  590. allow => 0,
  591. deny => 0,
  592. nomatch => 0,
  593. rate => 0.0,
  594. rate_last5m => 0.0,
  595. rate_max => 0.0
  596. },
  597. node_metrics => [
  598. #{
  599. node => node(),
  600. metrics => #{
  601. total => 0,
  602. allow => 0,
  603. deny => 0,
  604. nomatch => 0,
  605. rate => 0.0,
  606. rate_last5m => 0.0,
  607. rate_max => 0.0
  608. }
  609. }
  610. ],
  611. status => connected,
  612. node_status => [
  613. #{
  614. node => node(),
  615. status => connected
  616. }
  617. ]
  618. }
  619. }
  620. }.