• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

emqx / emqx / 12725937429

11 Jan 2025 04:46PM UTC coverage: 82.385%. First build
12725937429

Pull #14286

github

web-flow
Merge 356a7ec02 into 0fc8025be
Pull Request #14286: Implement node-level authentication/authorization cache

321 of 357 new or added lines in 30 files covered. (89.92%)

57681 of 70014 relevant lines covered (82.38%)

15154.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.87
/apps/emqx_auth/src/emqx_auth_template.erl
1
%%--------------------------------------------------------------------
2
%% Copyright (c) 2024-2025 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

17
-module(emqx_auth_template).
18

19
-include_lib("emqx/include/emqx_placeholder.hrl").
20
-include_lib("snabbkaffe/include/trace.hrl").
21

22
%% Template parsing/rendering
23
-export([
24
    parse_deep/2,
25
    parse_str/2,
26
    parse_sql/3,
27
    cache_key_template/1,
28
    cache_key/2,
29
    cache_key/3,
30
    placeholder_vars_from_str/1,
31
    render_deep_for_json/2,
32
    render_deep_for_url/2,
33
    render_deep_for_raw/2,
34
    render_str/2,
35
    render_urlencoded_str/2,
36
    render_sql_params/2,
37
    render_strict/2,
38
    escape_disallowed_placeholders_str/2,
39
    rename_client_info_vars/1
40
]).
41

42
-define(POSSIBLE_CACHE_KEY_IDS, [clientid, username, cert_common_name]).
43

44
-record(cache_key_template, {
45
    id :: binary(),
46
    vars :: emqx_template:t()
47
}).
48

49
-type var() :: emqx_template:varname() | {var_namespace, emqx_template:varname()}.
50
-type allowed_vars() :: [var()].
51
-type used_vars() :: [var()].
52
-type cache_key_template() :: #cache_key_template{}.
53
-type cache_key() :: fun(() -> term()).
54

55
%%--------------------------------------------------------------------
56
%% API
57
%%--------------------------------------------------------------------
58

59
-spec parse_deep(term(), allowed_vars()) -> {used_vars(), emqx_template:t()}.
60
parse_deep(Template, AllowedVars) ->
61
    Result = emqx_template:parse_deep(Template),
377✔
62
    handle_disallowed_placeholders(Result, AllowedVars, {deep, Template}).
377✔
63

64
-spec parse_str(unicode:chardata(), allowed_vars()) -> {used_vars(), emqx_template:t()}.
65
parse_str(Template, AllowedVars) ->
66
    Result = emqx_template:parse(Template),
7,423✔
67
    handle_disallowed_placeholders(Result, AllowedVars, {string, Template}).
7,423✔
68

69
-spec parse_sql(
70
    emqx_template_sql:raw_statement_template(), emqx_template_sql:sql_parameters(), allowed_vars()
71
) ->
72
    {
73
        used_vars(),
74
        emqx_template_sql:statement(),
75
        emqx_template_sql:row_template()
76
    }.
77
parse_sql(Template, ReplaceWith, AllowedVars) ->
78
    {Statement, Result} = emqx_template_sql:parse_prepstmt(
124✔
79
        Template,
80
        #{parameters => ReplaceWith, strip_double_quote => true}
81
    ),
82
    {UsedVars, TemplateWithAllowedVars} = handle_disallowed_placeholders(
124✔
83
        Result, AllowedVars, {string, Template}
84
    ),
85
    {UsedVars, Statement, TemplateWithAllowedVars}.
124✔
86

87
%% @doc Create a unique template from a list of variables.
88
%% The terms rendered from the templates will be the same if and only if
89
%% * the very template is the same
90
%% * the values provided for the template variables are the same
91
%%
92
%% Also, the template is unique, so it can be used as a cache key.
93
-spec cache_key_template(allowed_vars()) -> cache_key_template().
94
cache_key_template(Vars) ->
95
    #cache_key_template{
442✔
96
        id = list_to_binary(emqx_utils:gen_id()),
97
        vars = emqx_template:parse_deep(
98
            lists:map(
99
                fun(Var) ->
100
                    list_to_binary("${" ++ Var ++ "}")
707✔
101
                end,
102
                Vars
103
            )
104
        )
105
    }.
106

107
%% @doc Lazily render the cache key from the template and values.
108
-spec cache_key(map(), cache_key_template()) -> cache_key().
109
cache_key(Values, CacheKeyTemplate) ->
110
    cache_key(Values, CacheKeyTemplate, []).
627✔
111

112
-spec cache_key(map(), cache_key_template(), list()) -> cache_key().
113
cache_key(Values, #cache_key_template{id = TemplateId, vars = KeyVars}, ExtraKeyParts) when
114
    is_list(ExtraKeyParts)
115
->
116
    fun() ->
646✔
117
        %% We try to add some identifier to the cache key for better introspection.
118
        CacheKeyId = cache_template_id(first_present_kv(?POSSIBLE_CACHE_KEY_IDS, Values)),
72✔
119
        Key0 = render_deep_for_raw(KeyVars, Values),
72✔
120
        %% We hash the key to avoid storing the passwords or other sensitive data as-is in the cache.        %%
121
        %% TemplateId is used as some kind of salt.
122
        Key1 = crypto:hash(sha256, [TemplateId, Key0]),
72✔
123
        [CacheKeyId, Key1 | ExtraKeyParts]
72✔
124
    end.
125

126
-spec placeholder_vars_from_str(unicode:chardata()) -> [var()].
127
placeholder_vars_from_str(Str) ->
128
    emqx_template:placeholders(emqx_template:parse(Str)).
108✔
129

130
-spec escape_disallowed_placeholders_str(unicode:chardata(), allowed_vars()) -> term().
131
escape_disallowed_placeholders_str(Template, AllowedVars) ->
132
    ParsedTemplate = emqx_template:parse(Template),
101✔
133
    prerender_disallowed_placeholders(ParsedTemplate, AllowedVars).
101✔
134

135
-spec rename_client_info_vars(map()) -> map().
136
rename_client_info_vars(ClientInfo) ->
137
    Renames = [
1,723✔
138
        {cn, cert_common_name},
139
        {dn, cert_subject},
140
        {protocol, proto_name}
141
    ],
142
    lists:foldl(
1,723✔
143
        fun({Old, New}, Acc) ->
144
            emqx_utils_maps:rename(Old, New, Acc)
5,169✔
145
        end,
146
        ClientInfo,
147
        Renames
148
    ).
149

150
%%--------------------------------------------------------------------
151
%% Internal functions
152
%%--------------------------------------------------------------------
153

154
handle_disallowed_placeholders(Template, AllowedVars, Source) ->
155
    {UsedAllowedVars, UsedDisallowedVars} = emqx_template:placeholders(AllowedVars, Template),
7,924✔
156
    TemplateWithAllowedVars =
7,924✔
157
        case UsedDisallowedVars of
158
            [] ->
159
                Template;
7,912✔
160
            Disallowed ->
161
                ?tp(warning, "auth_template_invalid", #{
12✔
162
                    template => Source,
163
                    reason => Disallowed,
164
                    allowed => #{placeholders => AllowedVars},
165
                    notice =>
166
                        "Disallowed placeholders will be rendered as is."
167
                        " However, consider using `${$}` escaping for literal `$` where"
168
                        " needed to avoid unexpected results."
169
                }),
170
                Result = prerender_disallowed_placeholders(Template, AllowedVars),
12✔
171
                case Source of
12✔
172
                    {string, _} ->
173
                        emqx_template:parse(Result);
4✔
174
                    {deep, _} ->
175
                        emqx_template:parse_deep(Result)
8✔
176
                end
177
        end,
178
    {UsedAllowedVars, TemplateWithAllowedVars}.
7,924✔
179

180
prerender_disallowed_placeholders(Template, AllowedVars) ->
181
    {Result, _} = emqx_template:render(Template, #{}, #{
113✔
182
        var_trans => fun(Name, _) ->
183
            % NOTE
184
            % Rendering disallowed placeholders in escaped form, which will then
185
            % parse as a literal string.
186
            case lists:member(Name, AllowedVars) of
83✔
187
                true -> "${" ++ Name ++ "}";
69✔
188
                false -> "${$}{" ++ Name ++ "}"
14✔
189
            end
190
        end
191
    }),
192
    Result.
113✔
193

194
render_deep_for_json(Template, Credential) ->
195
    % NOTE
196
    % Ignoring errors here, undefined bindings will be replaced with empty string.
197
    {Term, _Errors} = emqx_template:render(
185✔
198
        Template,
199
        rename_client_info_vars(Credential),
200
        #{var_trans => fun to_string_for_json/2}
201
    ),
202
    Term.
183✔
203

204
render_deep_for_raw(Template, Credential) ->
205
    % NOTE
206
    % Ignoring errors here, undefined bindings will be replaced with empty string.
207
    {Term, _Errors} = emqx_template:render(
754✔
208
        Template,
209
        rename_client_info_vars(Credential),
210
        #{var_trans => fun to_string_for_raw/2}
211
    ),
212
    Term.
754✔
213

214
render_deep_for_url(Template, Credential) ->
215
    render_deep_for_raw(Template, Credential).
644✔
216

217
render_str(Template, Credential) ->
218
    % NOTE
219
    % Ignoring errors here, undefined bindings will be replaced with empty string.
220
    {String, _Errors} = emqx_template:render(
62✔
221
        Template,
222
        rename_client_info_vars(Credential),
223
        #{var_trans => fun to_string/2}
224
    ),
225
    unicode:characters_to_binary(String).
62✔
226

227
render_urlencoded_str(Template, Credential) ->
228
    % NOTE
229
    % Ignoring errors here, undefined bindings will be replaced with empty string.
230
    {String, _Errors} = emqx_template:render(
341✔
231
        Template,
232
        rename_client_info_vars(Credential),
233
        #{var_trans => fun to_urlencoded_string/2}
234
    ),
235
    unicode:characters_to_binary(String).
341✔
236

237
render_sql_params(ParamList, Credential) ->
238
    % NOTE
239
    % Ignoring errors here, undefined bindings will be replaced with empty string.
240
    {Row, _Errors} = emqx_template:render(
155✔
241
        ParamList,
242
        rename_client_info_vars(Credential),
243
        #{var_trans => fun to_sql_value/2}
244
    ),
245
    Row.
155✔
246

247
to_urlencoded_string(Name, Value) ->
248
    case uri_string:compose_query([{<<"q">>, to_string(Name, Value)}]) of
54✔
249
        <<"q=", EncodedBin/binary>> ->
250
            EncodedBin;
52✔
251
        "q=" ++ EncodedStr ->
252
            list_to_binary(EncodedStr)
2✔
253
    end.
254

255
to_string(Name, Value) ->
256
    emqx_template:to_string(render_var(Name, Value)).
117✔
257

258
%% This converter is to generate data structure possibly with non-utf8 strings.
259
%% It converts to unicode only strings (character lists).
260

261
to_string_for_raw(Name, Value) ->
262
    strings_to_unicode(Name, render_var(Name, Value)).
842✔
263

264
%% This converter is to generate data structure suitable for JSON serialization.
265
%% JSON strings are sequences of unicode characters, not bytes.
266
%% So we force all rendered data to be unicode, not only character lists.
267

268
to_string_for_json(Name, Value) ->
269
    all_to_unicode(Name, render_var(Name, Value)).
284✔
270

271
strings_to_unicode(_Name, Value) when is_binary(Value) ->
272
    Value;
777✔
273
strings_to_unicode(Name, Value) when is_list(Value) ->
274
    to_unicode_binary(Name, Value);
6✔
275
strings_to_unicode(_Name, Value) ->
276
    emqx_template:to_string(Value).
59✔
277

278
all_to_unicode(Name, Value) when is_list(Value) orelse is_binary(Value) ->
279
    to_unicode_binary(Name, Value);
269✔
280
all_to_unicode(_Name, Value) ->
281
    emqx_template:to_string(Value).
15✔
282

283
to_unicode_binary(Name, Value) when is_list(Value) orelse is_binary(Value) ->
284
    try unicode:characters_to_binary(Value) of
275✔
285
        Encoded when is_binary(Encoded) ->
286
            Encoded;
273✔
287
        _ ->
288
            error({encode_error, {non_unicode_data, Name}})
2✔
289
    catch
290
        error:badarg ->
291
            error({encode_error, {non_unicode_data, Name}})
×
292
    end.
293

294
to_sql_value(Name, Value) ->
295
    emqx_utils_sql:to_sql_value(render_var(Name, Value)).
165✔
296

297
render_var(_, undefined) ->
298
    % NOTE
299
    % Any allowed but undefined binding will be replaced with empty string, even when
300
    % rendering SQL values.
301
    <<>>;
24✔
302
render_var(?VAR_CERT_PEM, Value) ->
303
    base64:encode(Value);
4✔
304
render_var(?VAR_PEERHOST, Value) ->
305
    inet:ntoa(Value);
22✔
306
render_var(?VAR_PASSWORD, Value) ->
307
    iolist_to_binary(Value);
286✔
308
render_var(?VAR_PEERPORT, Value) ->
309
    integer_to_binary(Value);
4✔
310
render_var(_Name, Value) ->
311
    Value.
1,068✔
312

313
render_strict(Topic, ClientInfo) ->
314
    emqx_template:render_strict(Topic, rename_client_info_vars(ClientInfo)).
182✔
315

316
first_present_kv([Key | Keys], Map) ->
317
    case Map of
94✔
318
        #{Key := Value} when Value =/= undefined andalso Value =/= <<>> andalso Value =/= "" ->
319
            {Key, Value};
72✔
320
        _ ->
321
            first_present_kv(Keys, Map)
22✔
322
    end;
323
first_present_kv([], _) ->
NEW
324
    undefined.
×
325

326
cache_template_id(undefined) ->
NEW
327
    <<>>;
×
328
cache_template_id({Key, Value}) ->
329
    try emqx_utils_conv:bin(Value) of
72✔
330
        Bin ->
331
            KeyBin = atom_to_binary(Key, utf8),
72✔
332
            <<KeyBin/binary, ":", Bin/binary, "-">>
72✔
333
    catch
334
        error:_ ->
NEW
335
            <<>>
×
336
    end.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc