fix(auth): fix uri path handling

Fix uri path handling `emqx_connector_http`,
 HTTP authentication and authorization backends.
This commit is contained in:
Ilya Averyanov 2023-04-17 20:00:57 +03:00
parent 6123de73c4
commit 88ca94b417
9 changed files with 146 additions and 36 deletions

View File

@ -28,6 +28,7 @@
parse_sql/2, parse_sql/2,
render_deep/2, render_deep/2,
render_str/2, render_str/2,
render_urlencoded_str/2,
render_sql_params/2, render_sql_params/2,
is_superuser/1, is_superuser/1,
bin/1, bin/1,
@ -129,6 +130,13 @@ render_str(Template, Credential) ->
#{return => full_binary, var_trans => fun handle_var/2} #{return => full_binary, var_trans => fun handle_var/2}
). ).
render_urlencoded_str(Template, Credential) ->
emqx_placeholder:proc_tmpl(
Template,
mapping_credential(Credential),
#{return => full_binary, var_trans => fun urlencode_var/2}
).
render_sql_params(ParamList, Credential) -> render_sql_params(ParamList, Credential) ->
emqx_placeholder:proc_tmpl( emqx_placeholder:proc_tmpl(
ParamList, ParamList,
@ -217,6 +225,11 @@ without_password(Credential, [Name | Rest]) ->
without_password(Credential, Rest) without_password(Credential, Rest)
end. end.
urlencode_var({var, _} = Var, Value) ->
emqx_http_lib:uri_encode(handle_var(Var, Value));
urlencode_var(Var, Value) ->
handle_var(Var, Value).
handle_var({var, _Name}, undefined) -> handle_var({var, _Name}, undefined) ->
<<>>; <<>>;
handle_var({var, <<"peerhost">>}, PeerHost) -> handle_var({var, <<"peerhost">>}, PeerHost) ->

View File

@ -285,9 +285,9 @@ parse_url(Url) ->
BaseUrl = iolist_to_binary([Scheme, "//", HostPort]), BaseUrl = iolist_to_binary([Scheme, "//", HostPort]),
case string:split(Remaining, "?", leading) of case string:split(Remaining, "?", leading) of
[Path, QueryString] -> [Path, QueryString] ->
{BaseUrl, Path, QueryString}; {BaseUrl, <<"/", Path/binary>>, QueryString};
[Path] -> [Path] ->
{BaseUrl, Path, <<>>} {BaseUrl, <<"/", Path/binary>>, <<>>}
end; end;
[HostPort] -> [HostPort] ->
{iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>} {iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>}
@ -328,7 +328,7 @@ generate_request(Credential, #{
body_template := BodyTemplate body_template := BodyTemplate
}) -> }) ->
Headers = maps:to_list(Headers0), Headers = maps:to_list(Headers0),
Path = emqx_authn_utils:render_str(BasePathTemplate, Credential), Path = emqx_authn_utils:render_urlencoded_str(BasePathTemplate, Credential),
Query = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential), Query = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential),
Body = emqx_authn_utils:render_deep(BodyTemplate, Credential), Body = emqx_authn_utils:render_deep(BodyTemplate, Credential),
case Method of case Method of
@ -343,9 +343,9 @@ generate_request(Credential, #{
end. end.
append_query(Path, []) -> append_query(Path, []) ->
encode_path(Path); Path;
append_query(Path, Query) -> append_query(Path, Query) ->
encode_path(Path) ++ "?" ++ binary_to_list(qs(Query)). Path ++ "?" ++ binary_to_list(qs(Query)).
qs(KVs) -> qs(KVs) ->
qs(KVs, []). qs(KVs, []).
@ -407,10 +407,6 @@ parse_body(ContentType, _) ->
uri_encode(T) -> uri_encode(T) ->
emqx_http_lib:uri_encode(to_list(T)). emqx_http_lib:uri_encode(to_list(T)).
encode_path(Path) ->
Parts = string:split(Path, "/", all),
lists:flatten(["/" ++ Part || Part <- lists:map(fun uri_encode/1, Parts)]).
request_for_log(Credential, #{url := Url} = State) -> request_for_log(Credential, #{url := Url} = State) ->
SafeCredential = emqx_authn_utils:without_password(Credential), SafeCredential = emqx_authn_utils:without_password(Credential),
case generate_request(SafeCredential, State) of case generate_request(SafeCredential, State) of

View File

@ -47,7 +47,6 @@
}) })
). ).
-define(SERVER_RESPONSE_URLENCODE(Result), ?SERVER_RESPONSE_URLENCODE(Result, false)).
-define(SERVER_RESPONSE_URLENCODE(Result, IsSuperuser), -define(SERVER_RESPONSE_URLENCODE(Result, IsSuperuser),
list_to_binary( list_to_binary(
"result=" ++ "result=" ++
@ -166,6 +165,54 @@ test_user_auth(#{
?GLOBAL ?GLOBAL
). ).
t_authenticate_path_placeholders(_Config) ->
ok = emqx_authn_http_test_server:stop(),
{ok, _} = emqx_authn_http_test_server:start_link(?HTTP_PORT, <<"/[...]">>),
ok = emqx_authn_http_test_server:set_handler(
fun(Req0, State) ->
Req =
case cowboy_req:path(Req0) of
<<"/my/p%20ath//us%20er/auth//">> ->
cowboy_req:reply(
200,
#{<<"content-type">> => <<"application/json">>},
emqx_utils_json:encode(#{result => allow, is_superuser => false}),
Req0
);
Path ->
ct:pal("Unexpected path: ~p", [Path]),
cowboy_req:reply(403, Req0)
end,
{ok, Req, State}
end
),
Credentials = ?CREDENTIALS#{
username => <<"us er">>
},
AuthConfig = maps:merge(
raw_http_auth_config(),
#{
<<"url">> => <<"http://127.0.0.1:32333/my/p%20ath//${username}/auth//">>,
<<"body">> => #{}
}
),
{ok, _} = emqx:update_config(
?PATH,
{create_authenticator, ?GLOBAL, AuthConfig}
),
?assertMatch(
{ok, #{is_superuser := false}},
emqx_access_control:authenticate(Credentials)
),
_ = emqx_authn_test_lib:delete_authenticators(
[authentication],
?GLOBAL
).
t_no_value_for_placeholder(_Config) -> t_no_value_for_placeholder(_Config) ->
Handler = fun(Req0, State) -> Handler = fun(Req0, State) ->
{ok, RawBody, Req1} = cowboy_req:read_body(Req0), {ok, RawBody, Req1} = cowboy_req:read_body(Req0),

View File

@ -161,9 +161,9 @@ parse_url(Url) ->
BaseUrl = iolist_to_binary([Scheme, "//", HostPort]), BaseUrl = iolist_to_binary([Scheme, "//", HostPort]),
case string:split(Remaining, "?", leading) of case string:split(Remaining, "?", leading) of
[Path, QueryString] -> [Path, QueryString] ->
{BaseUrl, Path, QueryString}; {BaseUrl, <<"/", Path/binary>>, QueryString};
[Path] -> [Path] ->
{BaseUrl, Path, <<>>} {BaseUrl, <<"/", Path/binary>>, <<>>}
end; end;
[HostPort] -> [HostPort] ->
{iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>} {iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>}
@ -185,7 +185,7 @@ generate_request(
} }
) -> ) ->
Values = client_vars(Client, PubSub, Topic), Values = client_vars(Client, PubSub, Topic),
Path = emqx_authz_utils:render_str(BasePathTemplate, Values), Path = emqx_authz_utils:render_urlencoded_str(BasePathTemplate, Values),
Query = emqx_authz_utils:render_deep(BaseQueryTemplate, Values), Query = emqx_authz_utils:render_deep(BaseQueryTemplate, Values),
Body = emqx_authz_utils:render_deep(BodyTemplate, Values), Body = emqx_authz_utils:render_deep(BodyTemplate, Values),
case Method of case Method of
@ -202,9 +202,9 @@ generate_request(
end. end.
append_query(Path, []) -> append_query(Path, []) ->
encode_path(Path); to_list(Path);
append_query(Path, Query) -> append_query(Path, Query) ->
encode_path(Path) ++ "?" ++ to_list(query_string(Query)). to_list(Path) ++ "?" ++ to_list(query_string(Query)).
query_string(Body) -> query_string(Body) ->
query_string(Body, []). query_string(Body, []).
@ -222,10 +222,6 @@ query_string([{K, V} | More], Acc) ->
uri_encode(T) -> uri_encode(T) ->
emqx_http_lib:uri_encode(to_list(T)). emqx_http_lib:uri_encode(to_list(T)).
encode_path(Path) ->
Parts = string:split(Path, "/", all),
lists:flatten(["/" ++ Part || Part <- lists:map(fun uri_encode/1, Parts)]).
serialize_body(<<"application/json">>, Body) -> serialize_body(<<"application/json">>, Body) ->
emqx_utils_json:encode(Body); emqx_utils_json:encode(Body);
serialize_body(<<"application/x-www-form-urlencoded">>, Body) -> serialize_body(<<"application/x-www-form-urlencoded">>, Body) ->

View File

@ -16,7 +16,6 @@
-module(emqx_authz_utils). -module(emqx_authz_utils).
-include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("emqx_authz.hrl"). -include_lib("emqx_authz.hrl").
-export([ -export([
@ -28,6 +27,7 @@
update_config/2, update_config/2,
parse_deep/2, parse_deep/2,
parse_str/2, parse_str/2,
render_urlencoded_str/2,
parse_sql/3, parse_sql/3,
render_deep/2, render_deep/2,
render_str/2, render_str/2,
@ -128,6 +128,13 @@ render_str(Template, Values) ->
#{return => full_binary, var_trans => fun handle_var/2} #{return => full_binary, var_trans => fun handle_var/2}
). ).
render_urlencoded_str(Template, Values) ->
emqx_placeholder:proc_tmpl(
Template,
client_vars(Values),
#{return => full_binary, var_trans => fun urlencode_var/2}
).
render_sql_params(ParamList, Values) -> render_sql_params(ParamList, Values) ->
emqx_placeholder:proc_tmpl( emqx_placeholder:proc_tmpl(
ParamList, ParamList,
@ -181,6 +188,11 @@ convert_client_var({dn, DN}) -> {cert_subject, DN};
convert_client_var({protocol, Proto}) -> {proto_name, Proto}; convert_client_var({protocol, Proto}) -> {proto_name, Proto};
convert_client_var(Other) -> Other. convert_client_var(Other) -> Other.
urlencode_var({var, _} = Var, Value) ->
emqx_http_lib:uri_encode(handle_var(Var, Value));
urlencode_var(Var, Value) ->
handle_var(Var, Value).
handle_var({var, _Name}, undefined) -> handle_var({var, _Name}, undefined) ->
<<>>; <<>>;
handle_var({var, <<"peerhost">>}, IpAddr) -> handle_var({var, <<"peerhost">>}, IpAddr) ->

View File

@ -199,7 +199,7 @@ t_query_params(_Config) ->
peerhost := <<"127.0.0.1">>, peerhost := <<"127.0.0.1">>,
proto_name := <<"MQTT">>, proto_name := <<"MQTT">>,
mountpoint := <<"MOUNTPOINT">>, mountpoint := <<"MOUNTPOINT">>,
topic := <<"t">>, topic := <<"t/1">>,
action := <<"publish">> action := <<"publish">>
} = cowboy_req:match_qs( } = cowboy_req:match_qs(
[ [
@ -241,7 +241,7 @@ t_query_params(_Config) ->
?assertEqual( ?assertEqual(
allow, allow,
emqx_access_control:authorize(ClientInfo, publish, <<"t">>) emqx_access_control:authorize(ClientInfo, publish, <<"t/1">>)
). ).
t_path(_Config) -> t_path(_Config) ->
@ -249,13 +249,13 @@ t_path(_Config) ->
fun(Req0, State) -> fun(Req0, State) ->
?assertEqual( ?assertEqual(
<< <<
"/authz/users/" "/authz/use%20rs/"
"user%20name/" "user%20name/"
"client%20id/" "client%20id/"
"127.0.0.1/" "127.0.0.1/"
"MQTT/" "MQTT/"
"MOUNTPOINT/" "MOUNTPOINT/"
"t/1/" "t%2F1/"
"publish" "publish"
>>, >>,
cowboy_req:path(Req0) cowboy_req:path(Req0)
@ -264,7 +264,7 @@ t_path(_Config) ->
end, end,
#{ #{
<<"url">> => << <<"url">> => <<
"http://127.0.0.1:33333/authz/users/" "http://127.0.0.1:33333/authz/use%20rs/"
"${username}/" "${username}/"
"${clientid}/" "${clientid}/"
"${peerhost}/" "${peerhost}/"

View File

@ -47,7 +47,7 @@
namespace/0 namespace/0
]). ]).
-export([check_ssl_opts/2, validate_method/1]). -export([check_ssl_opts/2, validate_method/1, join_paths/2]).
-type connect_timeout() :: emqx_schema:duration() | infinity. -type connect_timeout() :: emqx_schema:duration() | infinity.
-type pool_type() :: random | hash. -type pool_type() :: random | hash.
@ -458,7 +458,7 @@ preprocess_request(
} = Req } = Req
) -> ) ->
#{ #{
method => emqx_plugin_libs_rule:preproc_tmpl(bin(Method)), method => emqx_plugin_libs_rule:preproc_tmpl(to_bin(Method)),
path => emqx_plugin_libs_rule:preproc_tmpl(Path), path => emqx_plugin_libs_rule:preproc_tmpl(Path),
body => maybe_preproc_tmpl(body, Req), body => maybe_preproc_tmpl(body, Req),
headers => preproc_headers(Headers), headers => preproc_headers(Headers),
@ -471,8 +471,8 @@ preproc_headers(Headers) when is_map(Headers) ->
fun(K, V, Acc) -> fun(K, V, Acc) ->
[ [
{ {
emqx_plugin_libs_rule:preproc_tmpl(bin(K)), emqx_plugin_libs_rule:preproc_tmpl(to_bin(K)),
emqx_plugin_libs_rule:preproc_tmpl(bin(V)) emqx_plugin_libs_rule:preproc_tmpl(to_bin(V))
} }
| Acc | Acc
] ]
@ -484,8 +484,8 @@ preproc_headers(Headers) when is_list(Headers) ->
lists:map( lists:map(
fun({K, V}) -> fun({K, V}) ->
{ {
emqx_plugin_libs_rule:preproc_tmpl(bin(K)), emqx_plugin_libs_rule:preproc_tmpl(to_bin(K)),
emqx_plugin_libs_rule:preproc_tmpl(bin(V)) emqx_plugin_libs_rule:preproc_tmpl(to_bin(V))
} }
end, end,
Headers Headers
@ -553,15 +553,41 @@ formalize_request(Method, BasePath, {Path, Headers, _Body}) when
-> ->
formalize_request(Method, BasePath, {Path, Headers}); formalize_request(Method, BasePath, {Path, Headers});
formalize_request(_Method, BasePath, {Path, Headers, Body}) -> formalize_request(_Method, BasePath, {Path, Headers, Body}) ->
{filename:join(BasePath, Path), Headers, Body}; {join_paths(BasePath, Path), Headers, Body};
formalize_request(_Method, BasePath, {Path, Headers}) -> formalize_request(_Method, BasePath, {Path, Headers}) ->
{filename:join(BasePath, Path), Headers}. {join_paths(BasePath, Path), Headers}.
bin(Bin) when is_binary(Bin) -> %% By default, we cannot treat HTTP paths as "file" or "resource" paths,
%% because an HTTP server may handle paths like
%% "/a/b/c/", "/a/b/c" and "/a//b/c" differently.
%%
%% So we try to avoid unneccessary path normalization.
%%
%% See also: `join_paths_test_/0`
join_paths(Path1, Path2) ->
do_join_paths(lists:reverse(to_list(Path1)), to_list(Path2)).
%% "abc/" + "/cde"
do_join_paths([$/ | Path1], [$/ | Path2]) ->
lists:reverse(Path1) ++ [$/ | Path2];
%% "abc/" + "cde"
do_join_paths([$/ | Path1], Path2) ->
lists:reverse(Path1) ++ [$/ | Path2];
%% "abc" + "/cde"
do_join_paths(Path1, [$/ | Path2]) ->
lists:reverse(Path1) ++ [$/ | Path2];
%% "abc" + "cde"
do_join_paths(Path1, Path2) ->
lists:reverse(Path1) ++ [$/ | Path2].
to_list(List) when is_list(List) -> List;
to_list(Bin) when is_binary(Bin) -> binary_to_list(Bin).
to_bin(Bin) when is_binary(Bin) ->
Bin; Bin;
bin(Str) when is_list(Str) -> to_bin(Str) when is_list(Str) ->
list_to_binary(Str); list_to_binary(Str);
bin(Atom) when is_atom(Atom) -> to_bin(Atom) when is_atom(Atom) ->
atom_to_binary(Atom, utf8). atom_to_binary(Atom, utf8).
reply_delegator(ReplyFunAndArgs, Result) -> reply_delegator(ReplyFunAndArgs, Result) ->
@ -642,4 +668,21 @@ redact_test_() ->
?_assertNotEqual(TestData2, redact(TestData2)) ?_assertNotEqual(TestData2, redact(TestData2))
]. ].
join_paths_test_() ->
[
?_assertEqual("abc/cde", join_paths("abc", "cde")),
?_assertEqual("abc/cde", join_paths("abc", "/cde")),
?_assertEqual("abc/cde", join_paths("abc/", "cde")),
?_assertEqual("abc/cde", join_paths("abc/", "/cde")),
?_assertEqual("/", join_paths("", "")),
?_assertEqual("/cde", join_paths("", "cde")),
?_assertEqual("/cde", join_paths("", "/cde")),
?_assertEqual("/cde", join_paths("/", "cde")),
?_assertEqual("/cde", join_paths("/", "/cde")),
?_assertEqual("//cde/", join_paths("/", "//cde/")),
?_assertEqual("abc///cde/", join_paths("abc//", "//cde/"))
].
-endif. -endif.

View File

@ -0,0 +1,3 @@
Fix HTTP path handling when composing the URL for the HTTP requests in authentication and authorization modules.
* Avoid unnecessary URL normalization since we cannot assume that external servers treat original and normalized URLs equally. This led to bugs like [#10411](https://github.com/emqx/emqx/issues/10411).
* Fix the issue that path segments could be HTTP encoded twice.

View File