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:
parent
3136ec5958
commit
22838f027a
|
@ -31,12 +31,14 @@
|
||||||
-define(PH_CERT_SUBJECT, ?PH(?VAR_CERT_SUBJECT)).
|
-define(PH_CERT_SUBJECT, ?PH(?VAR_CERT_SUBJECT)).
|
||||||
-define(PH_CERT_CN_NAME, ?PH(?VAR_CERT_CN_NAME)).
|
-define(PH_CERT_CN_NAME, ?PH(?VAR_CERT_CN_NAME)).
|
||||||
|
|
||||||
%% MQTT
|
%% MQTT/Gateway
|
||||||
-define(VAR_PASSWORD, "password").
|
-define(VAR_PASSWORD, "password").
|
||||||
-define(VAR_CLIENTID, "clientid").
|
-define(VAR_CLIENTID, "clientid").
|
||||||
-define(VAR_USERNAME, "username").
|
-define(VAR_USERNAME, "username").
|
||||||
-define(VAR_TOPIC, "topic").
|
-define(VAR_TOPIC, "topic").
|
||||||
|
-define(VAR_ENDPOINT_NAME, "endpoint_name").
|
||||||
-define(VAR_NS_CLIENT_ATTRS, {var_namespace, "client_attrs"}).
|
-define(VAR_NS_CLIENT_ATTRS, {var_namespace, "client_attrs"}).
|
||||||
|
|
||||||
-define(PH_PASSWORD, ?PH(?VAR_PASSWORD)).
|
-define(PH_PASSWORD, ?PH(?VAR_PASSWORD)).
|
||||||
-define(PH_CLIENTID, ?PH(?VAR_CLIENTID)).
|
-define(PH_CLIENTID, ?PH(?VAR_CLIENTID)).
|
||||||
-define(PH_FROM_CLIENTID, ?PH("from_clientid")).
|
-define(PH_FROM_CLIENTID, ?PH("from_clientid")).
|
||||||
|
@ -90,7 +92,7 @@
|
||||||
-define(PH_NODE, ?PH("node")).
|
-define(PH_NODE, ?PH("node")).
|
||||||
-define(PH_REASON, ?PH("reason")).
|
-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(VAR_RETAIN, "retain").
|
||||||
-define(PH_RETAIN, ?PH(?VAR_RETAIN)).
|
-define(PH_RETAIN, ?PH(?VAR_RETAIN)).
|
||||||
|
|
||||||
|
|
|
@ -28,10 +28,19 @@
|
||||||
|
|
||||||
-export([replvar/2]).
|
-export([replvar/2]).
|
||||||
|
|
||||||
|
-export([lookup/2]).
|
||||||
|
|
||||||
-export_type([mountpoint/0]).
|
-export_type([mountpoint/0]).
|
||||||
|
|
||||||
-type mountpoint() :: binary().
|
-type mountpoint() :: binary().
|
||||||
|
|
||||||
|
-define(ALLOWED_VARS, [
|
||||||
|
?VAR_CLIENTID,
|
||||||
|
?VAR_USERNAME,
|
||||||
|
?VAR_ENDPOINT_NAME,
|
||||||
|
?VAR_NS_CLIENT_ATTRS
|
||||||
|
]).
|
||||||
|
|
||||||
-spec mount(option(mountpoint()), Any) -> Any when
|
-spec mount(option(mountpoint()), Any) -> Any when
|
||||||
Any ::
|
Any ::
|
||||||
emqx_types:topic()
|
emqx_types:topic()
|
||||||
|
@ -87,12 +96,29 @@ unmount_maybe_share(MountPoint, TopicFilter = #share{topic = Topic}) when
|
||||||
-spec replvar(option(mountpoint()), map()) -> option(mountpoint()).
|
-spec replvar(option(mountpoint()), map()) -> option(mountpoint()).
|
||||||
replvar(undefined, _Vars) ->
|
replvar(undefined, _Vars) ->
|
||||||
undefined;
|
undefined;
|
||||||
replvar(MountPoint, Vars0) ->
|
replvar(MountPoint, Vars) ->
|
||||||
Allowed = [clientid, username, endpoint_name, client_attrs],
|
Template = parse(MountPoint),
|
||||||
Vars = maps:filter(
|
{String, _Errors} = emqx_template:render(Template, {?MODULE, Vars}),
|
||||||
fun(K, V) -> V =/= undefined andalso lists:member(K, Allowed) end,
|
|
||||||
Vars0
|
|
||||||
),
|
|
||||||
Template = emqx_template:parse(MountPoint),
|
|
||||||
{String, _Errors} = emqx_template:render(Template, Vars),
|
|
||||||
unicode:characters_to_binary(String).
|
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.
|
||||||
|
|
|
@ -116,4 +116,32 @@ t_replvar(_) ->
|
||||||
username => undefined
|
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">>
|
||||||
|
}
|
||||||
|
)
|
||||||
).
|
).
|
||||||
|
|
|
@ -139,7 +139,7 @@ handle_disallowed_placeholders(Template, Source, Allowed) ->
|
||||||
" However, consider using `${$}` escaping for literal `$` where"
|
" However, consider using `${$}` escaping for literal `$` where"
|
||||||
" needed to avoid unexpected results."
|
" needed to avoid unexpected results."
|
||||||
}),
|
}),
|
||||||
Result = prerender_disallowed_placeholders(Template, Allowed),
|
Result = emqx_template:escape_disallowed(Template, Allowed),
|
||||||
case Source of
|
case Source of
|
||||||
{string, _} ->
|
{string, _} ->
|
||||||
emqx_template:parse(Result);
|
emqx_template:parse(Result);
|
||||||
|
@ -148,20 +148,6 @@ handle_disallowed_placeholders(Template, Source, Allowed) ->
|
||||||
end
|
end
|
||||||
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) ->
|
render_deep(Template, Values) ->
|
||||||
% NOTE
|
% NOTE
|
||||||
% Ignoring errors here, undefined bindings will be replaced with empty string.
|
% Ignoring errors here, undefined bindings will be replaced with empty string.
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
-export([lookup/2]).
|
-export([lookup/2]).
|
||||||
|
|
||||||
-export([to_string/1]).
|
-export([to_string/1]).
|
||||||
|
-export([escape_disallowed/2]).
|
||||||
|
|
||||||
-export_type([t/0]).
|
-export_type([t/0]).
|
||||||
-export_type([str/0]).
|
-export_type([str/0]).
|
||||||
|
@ -157,9 +158,31 @@ validate(Allowed, Template) ->
|
||||||
{error, [{Var, disallowed} || Var <- Disallowed]}
|
{error, [{Var, disallowed} || Var <- Disallowed]}
|
||||||
end.
|
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) ->
|
find_disallowed(Vars, Allowed) ->
|
||||||
lists:filter(fun(Var) -> not is_allowed(Var, Allowed) end, Vars).
|
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, []) ->
|
is_allowed(_Var, []) ->
|
||||||
false;
|
false;
|
||||||
is_allowed(Var, [{var_namespace, VarPrefix} | Allowed]) ->
|
is_allowed(Var, [{var_namespace, VarPrefix} | Allowed]) ->
|
||||||
|
|
Loading…
Reference in New Issue