emqx_auth_utils.erl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 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_auth_utils).
  17. -include_lib("emqx/include/emqx_placeholder.hrl").
  18. -include_lib("snabbkaffe/include/trace.hrl").
  19. %% Template parsing/rendering
  20. -export([
  21. parse_deep/2,
  22. parse_str/2,
  23. parse_sql/3,
  24. render_deep_for_json/2,
  25. render_deep_for_url/2,
  26. render_deep_for_raw/2,
  27. render_str/2,
  28. render_urlencoded_str/2,
  29. render_sql_params/2
  30. ]).
  31. %% URL parsing
  32. -export([parse_url/1]).
  33. %% HTTP request/response helpers
  34. -export([generate_request/2]).
  35. -define(DEFAULT_HTTP_REQUEST_CONTENT_TYPE, <<"application/json">>).
  36. %%--------------------------------------------------------------------
  37. %% API
  38. %%--------------------------------------------------------------------
  39. %%--------------------------------------------------------------------
  40. %% Template parsing/rendering
  41. parse_deep(Template, AllowedVars) ->
  42. Result = emqx_template:parse_deep(Template),
  43. handle_disallowed_placeholders(Result, AllowedVars, {deep, Template}).
  44. parse_str(Template, AllowedVars) ->
  45. Result = emqx_template:parse(Template),
  46. handle_disallowed_placeholders(Result, AllowedVars, {string, Template}).
  47. parse_sql(Template, ReplaceWith, AllowedVars) ->
  48. {Statement, Result} = emqx_template_sql:parse_prepstmt(
  49. Template,
  50. #{parameters => ReplaceWith, strip_double_quote => true}
  51. ),
  52. {Statement, handle_disallowed_placeholders(Result, AllowedVars, {string, Template})}.
  53. handle_disallowed_placeholders(Template, AllowedVars, Source) ->
  54. case emqx_template:validate(AllowedVars, Template) of
  55. ok ->
  56. Template;
  57. {error, Disallowed} ->
  58. ?tp(warning, "auth_template_invalid", #{
  59. template => Source,
  60. reason => Disallowed,
  61. allowed => #{placeholders => AllowedVars},
  62. notice =>
  63. "Disallowed placeholders will be rendered as is."
  64. " However, consider using `${$}` escaping for literal `$` where"
  65. " needed to avoid unexpected results."
  66. }),
  67. Result = prerender_disallowed_placeholders(Template, AllowedVars),
  68. case Source of
  69. {string, _} ->
  70. emqx_template:parse(Result);
  71. {deep, _} ->
  72. emqx_template:parse_deep(Result)
  73. end
  74. end.
  75. prerender_disallowed_placeholders(Template, AllowedVars) ->
  76. {Result, _} = emqx_template:render(Template, #{}, #{
  77. var_trans => fun(Name, _) ->
  78. % NOTE
  79. % Rendering disallowed placeholders in escaped form, which will then
  80. % parse as a literal string.
  81. case lists:member(Name, AllowedVars) of
  82. true -> "${" ++ Name ++ "}";
  83. false -> "${$}{" ++ Name ++ "}"
  84. end
  85. end
  86. }),
  87. Result.
  88. render_deep_for_json(Template, Credential) ->
  89. % NOTE
  90. % Ignoring errors here, undefined bindings will be replaced with empty string.
  91. {Term, _Errors} = emqx_template:render(
  92. Template,
  93. rename_client_info_vars(Credential),
  94. #{var_trans => fun to_string_for_json/2}
  95. ),
  96. Term.
  97. render_deep_for_raw(Template, Credential) ->
  98. % NOTE
  99. % Ignoring errors here, undefined bindings will be replaced with empty string.
  100. {Term, _Errors} = emqx_template:render(
  101. Template,
  102. rename_client_info_vars(Credential),
  103. #{var_trans => fun to_string_for_raw/2}
  104. ),
  105. Term.
  106. render_deep_for_url(Template, Credential) ->
  107. render_deep_for_raw(Template, Credential).
  108. render_str(Template, Credential) ->
  109. % NOTE
  110. % Ignoring errors here, undefined bindings will be replaced with empty string.
  111. {String, _Errors} = emqx_template:render(
  112. Template,
  113. rename_client_info_vars(Credential),
  114. #{var_trans => fun to_string/2}
  115. ),
  116. unicode:characters_to_binary(String).
  117. render_urlencoded_str(Template, Credential) ->
  118. % NOTE
  119. % Ignoring errors here, undefined bindings will be replaced with empty string.
  120. {String, _Errors} = emqx_template:render(
  121. Template,
  122. rename_client_info_vars(Credential),
  123. #{var_trans => fun to_urlencoded_string/2}
  124. ),
  125. unicode:characters_to_binary(String).
  126. render_sql_params(ParamList, Credential) ->
  127. % NOTE
  128. % Ignoring errors here, undefined bindings will be replaced with empty string.
  129. {Row, _Errors} = emqx_template:render(
  130. ParamList,
  131. rename_client_info_vars(Credential),
  132. #{var_trans => fun to_sql_value/2}
  133. ),
  134. Row.
  135. to_urlencoded_string(Name, Value) ->
  136. case uri_string:compose_query([{<<"q">>, to_string(Name, Value)}]) of
  137. <<"q=", EncodedBin/binary>> ->
  138. EncodedBin;
  139. "q=" ++ EncodedStr ->
  140. list_to_binary(EncodedStr)
  141. end.
  142. to_string(Name, Value) ->
  143. emqx_template:to_string(render_var(Name, Value)).
  144. %% This converter is to generate data structure possibly with non-utf8 strings.
  145. %% It converts to unicode only strings (character lists).
  146. to_string_for_raw(Name, Value) ->
  147. strings_to_unicode(Name, render_var(Name, Value)).
  148. %% This converter is to generate data structure suitable for JSON serialization.
  149. %% JSON strings are sequences of unicode characters, not bytes.
  150. %% So we force all rendered data to be unicode, not only character lists.
  151. to_string_for_json(Name, Value) ->
  152. all_to_unicode(Name, render_var(Name, Value)).
  153. strings_to_unicode(_Name, Value) when is_binary(Value) ->
  154. Value;
  155. strings_to_unicode(Name, Value) when is_list(Value) ->
  156. to_unicode_binary(Name, Value);
  157. strings_to_unicode(_Name, Value) ->
  158. emqx_template:to_string(Value).
  159. all_to_unicode(Name, Value) when is_list(Value) orelse is_binary(Value) ->
  160. to_unicode_binary(Name, Value);
  161. all_to_unicode(_Name, Value) ->
  162. emqx_template:to_string(Value).
  163. to_unicode_binary(Name, Value) when is_list(Value) orelse is_binary(Value) ->
  164. try unicode:characters_to_binary(Value) of
  165. Encoded when is_binary(Encoded) ->
  166. Encoded;
  167. _ ->
  168. error({encode_error, {non_unicode_data, Name}})
  169. catch
  170. error:badarg ->
  171. error({encode_error, {non_unicode_data, Name}})
  172. end.
  173. to_sql_value(Name, Value) ->
  174. emqx_utils_sql:to_sql_value(render_var(Name, Value)).
  175. render_var(_, undefined) ->
  176. % NOTE
  177. % Any allowed but undefined binding will be replaced with empty string, even when
  178. % rendering SQL values.
  179. <<>>;
  180. render_var(?VAR_PEERHOST, Value) ->
  181. inet:ntoa(Value);
  182. render_var(?VAR_PASSWORD, Value) ->
  183. iolist_to_binary(Value);
  184. render_var(_Name, Value) ->
  185. Value.
  186. rename_client_info_vars(ClientInfo) ->
  187. Renames = [
  188. {cn, cert_common_name},
  189. {dn, cert_subject},
  190. {protocol, proto_name}
  191. ],
  192. lists:foldl(
  193. fun({Old, New}, Acc) ->
  194. emqx_utils_maps:rename(Old, New, Acc)
  195. end,
  196. ClientInfo,
  197. Renames
  198. ).
  199. %%--------------------------------------------------------------------
  200. %% URL parsing
  201. -spec parse_url(binary()) ->
  202. {_Base :: emqx_utils_uri:request_base(), _Path :: binary(), _Query :: binary()}.
  203. parse_url(Url) ->
  204. Parsed = emqx_utils_uri:parse(Url),
  205. case Parsed of
  206. #{scheme := undefined} ->
  207. throw({invalid_url, {no_scheme, Url}});
  208. #{authority := undefined} ->
  209. throw({invalid_url, {no_host, Url}});
  210. #{authority := #{userinfo := Userinfo}} when Userinfo =/= undefined ->
  211. throw({invalid_url, {userinfo_not_supported, Url}});
  212. #{fragment := Fragment} when Fragment =/= undefined ->
  213. throw({invalid_url, {fragments_not_supported, Url}});
  214. _ ->
  215. case emqx_utils_uri:request_base(Parsed) of
  216. {ok, Base} ->
  217. {Base, emqx_utils_uri:path(Parsed),
  218. emqx_maybe:define(emqx_utils_uri:query(Parsed), <<>>)};
  219. {error, Reason} ->
  220. throw({invalid_url, {invalid_base, Reason, Url}})
  221. end
  222. end.
  223. %%--------------------------------------------------------------------
  224. %% HTTP request/response helpers
  225. generate_request(
  226. #{
  227. method := Method,
  228. headers := Headers,
  229. base_path_template := BasePathTemplate,
  230. base_query_template := BaseQueryTemplate,
  231. body_template := BodyTemplate
  232. },
  233. Values
  234. ) ->
  235. Path = render_urlencoded_str(BasePathTemplate, Values),
  236. Query = render_deep_for_url(BaseQueryTemplate, Values),
  237. case Method of
  238. get ->
  239. Body = render_deep_for_url(BodyTemplate, Values),
  240. NPath = append_query(Path, Query, Body),
  241. {ok, {NPath, Headers}};
  242. _ ->
  243. try
  244. ContentType = post_request_content_type(Headers),
  245. Body = serialize_body(ContentType, BodyTemplate, Values),
  246. NPathQuery = append_query(Path, Query),
  247. {ok, {NPathQuery, Headers, Body}}
  248. catch
  249. error:{encode_error, _} = Reason ->
  250. {error, Reason}
  251. end
  252. end.
  253. post_request_content_type(Headers) ->
  254. proplists:get_value(<<"content-type">>, Headers, ?DEFAULT_HTTP_REQUEST_CONTENT_TYPE).
  255. append_query(Path, []) ->
  256. Path;
  257. append_query(Path, Query) ->
  258. [Path, $?, uri_string:compose_query(Query)].
  259. append_query(Path, Query, Body) ->
  260. append_query(Path, Query ++ maps:to_list(Body)).
  261. serialize_body(<<"application/json">>, BodyTemplate, ClientInfo) ->
  262. Body = emqx_auth_utils:render_deep_for_json(BodyTemplate, ClientInfo),
  263. emqx_utils_json:encode(Body);
  264. serialize_body(<<"application/x-www-form-urlencoded">>, BodyTemplate, ClientInfo) ->
  265. Body = emqx_auth_utils:render_deep_for_url(BodyTemplate, ClientInfo),
  266. uri_string:compose_query(maps:to_list(Body));
  267. serialize_body(undefined, _BodyTemplate, _ClientInfo) ->
  268. throw(missing_content_type_header);
  269. serialize_body(ContentType, _BodyTemplate, _ClientInfo) ->
  270. throw({unknown_content_type_header_value, ContentType}).
  271. -ifdef(TEST).
  272. -include_lib("eunit/include/eunit.hrl").
  273. templates_test_() ->
  274. [
  275. ?_assertEqual(
  276. {
  277. #{port => 80, scheme => http, host => "example.com"},
  278. <<"">>,
  279. <<"client=${clientid}">>
  280. },
  281. parse_url(<<"http://example.com?client=${clientid}">>)
  282. ),
  283. ?_assertEqual(
  284. {
  285. #{port => 80, scheme => http, host => "example.com"},
  286. <<"/path">>,
  287. <<"client=${clientid}">>
  288. },
  289. parse_url(<<"http://example.com/path?client=${clientid}">>)
  290. ),
  291. ?_assertEqual(
  292. {#{port => 80, scheme => http, host => "example.com"}, <<"/path">>, <<>>},
  293. parse_url(<<"http://example.com/path">>)
  294. )
  295. ].
  296. -endif.