fix: mountpoint template render should not replace unknown as undefined

For backward compatibility, the unknown vars used in mountpoint
is kept unchanged.
e.g. '${unknown}/foo/bar' should be rendered as '${unknown}/foo/bar'
but not 'undefined/foo/bar'
This commit is contained in:
zmstone 2024-03-22 20:57:36 +01:00
parent 3136ec5958
commit 22838f027a
5 changed files with 90 additions and 25 deletions

View File

@ -31,12 +31,14 @@
-define(PH_CERT_SUBJECT, ?PH(?VAR_CERT_SUBJECT)).
-define(PH_CERT_CN_NAME, ?PH(?VAR_CERT_CN_NAME)).
%% MQTT
%% MQTT/Gateway
-define(VAR_PASSWORD, "password").
-define(VAR_CLIENTID, "clientid").
-define(VAR_USERNAME, "username").
-define(VAR_TOPIC, "topic").
-define(VAR_ENDPOINT_NAME, "endpoint_name").
-define(VAR_NS_CLIENT_ATTRS, {var_namespace, "client_attrs"}).
-define(PH_PASSWORD, ?PH(?VAR_PASSWORD)).
-define(PH_CLIENTID, ?PH(?VAR_CLIENTID)).
-define(PH_FROM_CLIENTID, ?PH("from_clientid")).
@ -90,7 +92,7 @@
-define(PH_NODE, ?PH("node")).
-define(PH_REASON, ?PH("reason")).
-define(PH_ENDPOINT_NAME, ?PH("endpoint_name")).
-define(PH_ENDPOINT_NAME, ?PH(?VAR_ENDPOINT_NAME)).
-define(VAR_RETAIN, "retain").
-define(PH_RETAIN, ?PH(?VAR_RETAIN)).

View File

@ -28,10 +28,19 @@
-export([replvar/2]).
-export([lookup/2]).
-export_type([mountpoint/0]).
-type mountpoint() :: binary().
-define(ALLOWED_VARS, [
?VAR_CLIENTID,
?VAR_USERNAME,
?VAR_ENDPOINT_NAME,
?VAR_NS_CLIENT_ATTRS
]).
-spec mount(option(mountpoint()), Any) -> Any when
Any ::
emqx_types:topic()
@ -87,12 +96,29 @@ unmount_maybe_share(MountPoint, TopicFilter = #share{topic = Topic}) when
-spec replvar(option(mountpoint()), map()) -> option(mountpoint()).
replvar(undefined, _Vars) ->
undefined;
replvar(MountPoint, Vars0) ->
Allowed = [clientid, username, endpoint_name, client_attrs],
Vars = maps:filter(
fun(K, V) -> V =/= undefined andalso lists:member(K, Allowed) end,
Vars0
),
Template = emqx_template:parse(MountPoint),
{String, _Errors} = emqx_template:render(Template, Vars),
replvar(MountPoint, Vars) ->
Template = parse(MountPoint),
{String, _Errors} = emqx_template:render(Template, {?MODULE, Vars}),
unicode:characters_to_binary(String).
lookup([<<?VAR_CLIENTID>>], #{clientid := ClientId}) when is_binary(ClientId) ->
{ok, ClientId};
lookup([<<?VAR_USERNAME>>], #{username := Username}) when is_binary(Username) ->
{ok, Username};
lookup([<<?VAR_ENDPOINT_NAME>>], #{endpoint_name := Name}) when is_binary(Name) ->
{ok, Name};
lookup([<<"client_attrs">>, AttrName], #{client_attrs := Attrs}) when is_map(Attrs) ->
Original = iolist_to_binary(["${client_attrs.", AttrName, "}"]),
{ok, maps:get(AttrName, Attrs, Original)};
lookup(Accessor, _) ->
{ok, iolist_to_binary(["${", lists:join(".", Accessor), "}"])}.
parse(Template) ->
Parsed = emqx_template:parse(Template),
case emqx_template:validate(?ALLOWED_VARS, Parsed) of
ok ->
Parsed;
{error, _Disallowed} ->
Escaped = emqx_template:escape_disallowed(Parsed, ?ALLOWED_VARS),
emqx_template:parse(Escaped)
end.

View File

@ -116,4 +116,32 @@ t_replvar(_) ->
username => undefined
}
)
),
?assertEqual(
<<"mount/g1/clientid/">>,
replvar(
<<"mount/${client_attrs.group}/${clientid}/">>,
#{
clientid => <<"clientid">>,
client_attrs => #{<<"group">> => <<"g1">>}
}
)
),
?assertEqual(
<<"mount/${client_attrs.group}/clientid/">>,
replvar(
<<"mount/${client_attrs.group}/${clientid}/">>,
#{
clientid => <<"clientid">>
}
)
),
?assertEqual(
<<"mount/${not.allowed}/clientid/">>,
replvar(
<<"mount/${not.allowed}/${clientid}/">>,
#{
clientid => <<"clientid">>
}
)
).

View File

@ -139,7 +139,7 @@ handle_disallowed_placeholders(Template, Source, Allowed) ->
" However, consider using `${$}` escaping for literal `$` where"
" needed to avoid unexpected results."
}),
Result = prerender_disallowed_placeholders(Template, Allowed),
Result = emqx_template:escape_disallowed(Template, Allowed),
case Source of
{string, _} ->
emqx_template:parse(Result);
@ -148,20 +148,6 @@ handle_disallowed_placeholders(Template, Source, Allowed) ->
end
end.
prerender_disallowed_placeholders(Template, Allowed) ->
{Result, _} = emqx_template:render(Template, #{}, #{
var_trans => fun(Name, _) ->
% NOTE
% Rendering disallowed placeholders in escaped form, which will then
% parse as a literal string.
case lists:member(Name, Allowed) of
true -> "${" ++ Name ++ "}";
false -> "${$}{" ++ Name ++ "}"
end
end
}),
Result.
render_deep(Template, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.

View File

@ -32,6 +32,7 @@
-export([lookup/2]).
-export([to_string/1]).
-export([escape_disallowed/2]).
-export_type([t/0]).
-export_type([str/0]).
@ -157,9 +158,31 @@ validate(Allowed, Template) ->
{error, [{Var, disallowed} || Var <- Disallowed]}
end.
%% @doc Escape `$' with `${$}' for the variable references
%% which are not allowed, so the original variable name
%% can be preserved instead of rendered as `undefined'.
%% E.g. to render `${var1}/${clientid}', if only `clientid'
%% is allowed, the rendering result should be `${var1}/client1'
%% but not `undefined/client1'.
escape_disallowed(Template, Allowed) ->
{Result, _} = render(Template, #{}, #{
var_trans => fun(Name, _) ->
case is_allowed(Name, Allowed) of
true -> "${" ++ Name ++ "}";
false -> "${$}{" ++ Name ++ "}"
end
end
}),
Result.
find_disallowed(Vars, Allowed) ->
lists:filter(fun(Var) -> not is_allowed(Var, Allowed) end, Vars).
%% @private Return 'true' if a variable reference matches
%% at least one allowed variables.
%% For `"${var_name}"' kind of reference, its a `=:=' compare
%% for `{var_namespace, "namespace"}' kind of reference
%% it matches the `"namespace."' prefix.
is_allowed(_Var, []) ->
false;
is_allowed(Var, [{var_namespace, VarPrefix} | Allowed]) ->