chore(auth_http): unify http request generation

Co-authored-by: Thales Macedo Garitezi <thalesmg@gmail.com>
This commit is contained in:
Ilya Averyanov 2024-05-14 20:35:22 +03:00
parent bca3782d73
commit daf2e5a444
21 changed files with 483 additions and 532 deletions

View File

@ -21,8 +21,6 @@
-define(AUTHN, emqx_authn_chains). -define(AUTHN, emqx_authn_chains).
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}").
%% has to be the same as the root field name defined in emqx_schema %% has to be the same as the root field name defined in emqx_schema
-define(CONF_NS, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME). -define(CONF_NS, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME).
-define(CONF_NS_ATOM, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM). -define(CONF_NS_ATOM, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
@ -32,4 +30,16 @@
-define(AUTHN_RESOURCE_GROUP, <<"emqx_authn">>). -define(AUTHN_RESOURCE_GROUP, <<"emqx_authn">>).
%% VAR_NS_CLIENT_ATTRS is added here because it can be initialized before authn.
%% NOTE: authn return may add more to (or even overwrite) client_attrs.
-define(AUTHN_DEFAULT_ALLOWED_VARS, [
?VAR_USERNAME,
?VAR_CLIENTID,
?VAR_PASSWORD,
?VAR_PEERHOST,
?VAR_CERT_SUBJECT,
?VAR_CERT_CN_NAME,
?VAR_NS_CLIENT_ATTRS
]).
-endif. -endif.

View File

@ -38,8 +38,6 @@
-define(ROOT_KEY, [authorization]). -define(ROOT_KEY, [authorization]).
-define(CONF_KEY_PATH, [authorization, sources]). -define(CONF_KEY_PATH, [authorization, sources]).
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9_]+\\}").
%% has to be the same as the root field name defined in emqx_schema %% has to be the same as the root field name defined in emqx_schema
-define(CONF_NS, ?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME). -define(CONF_NS, ?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME).
-define(CONF_NS_ATOM, ?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_ATOM). -define(CONF_NS_ATOM, ?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_ATOM).

View File

@ -16,15 +16,221 @@
-module(emqx_auth_utils). -module(emqx_auth_utils).
%% TODO -include_lib("emqx/include/emqx_placeholder.hrl").
%% Move more identical authn and authz helpers here -include_lib("snabbkaffe/include/trace.hrl").
%% Template parsing/rendering
-export([
parse_deep/2,
parse_str/2,
parse_sql/3,
render_deep_for_json/2,
render_deep_for_url/2,
render_deep_for_raw/2,
render_str/2,
render_urlencoded_str/2,
render_sql_params/2
]).
%% URL parsing
-export([parse_url/1]). -export([parse_url/1]).
%% HTTP request/response helpers
-export([generate_request/2]).
-define(DEFAULT_HTTP_REQUEST_CONTENT_TYPE, <<"application/json">>).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% API %% API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%%--------------------------------------------------------------------
%% Template parsing/rendering
parse_deep(Template, AllowedVars) ->
Result = emqx_template:parse_deep(Template),
handle_disallowed_placeholders(Result, AllowedVars, {deep, Template}).
parse_str(Template, AllowedVars) ->
Result = emqx_template:parse(Template),
handle_disallowed_placeholders(Result, AllowedVars, {string, Template}).
parse_sql(Template, ReplaceWith, AllowedVars) ->
{Statement, Result} = emqx_template_sql:parse_prepstmt(
Template,
#{parameters => ReplaceWith, strip_double_quote => true}
),
{Statement, handle_disallowed_placeholders(Result, AllowedVars, {string, Template})}.
handle_disallowed_placeholders(Template, AllowedVars, Source) ->
case emqx_template:validate(AllowedVars, Template) of
ok ->
Template;
{error, Disallowed} ->
?tp(warning, "auth_template_invalid", #{
template => Source,
reason => Disallowed,
allowed => #{placeholders => AllowedVars},
notice =>
"Disallowed placeholders will be rendered as is."
" However, consider using `${$}` escaping for literal `$` where"
" needed to avoid unexpected results."
}),
Result = prerender_disallowed_placeholders(Template, AllowedVars),
case Source of
{string, _} ->
emqx_template:parse(Result);
{deep, _} ->
emqx_template:parse_deep(Result)
end
end.
prerender_disallowed_placeholders(Template, AllowedVars) ->
{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, AllowedVars) of
true -> "${" ++ Name ++ "}";
false -> "${$}{" ++ Name ++ "}"
end
end
}),
Result.
render_deep_for_json(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Term, _Errors} = emqx_template:render(
Template,
rename_client_info_vars(Credential),
#{var_trans => fun to_string_for_json/2}
),
Term.
render_deep_for_raw(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Term, _Errors} = emqx_template:render(
Template,
rename_client_info_vars(Credential),
#{var_trans => fun to_string_for_raw/2}
),
Term.
render_deep_for_url(Template, Credential) ->
render_deep_for_raw(Template, Credential).
render_str(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_template:render(
Template,
rename_client_info_vars(Credential),
#{var_trans => fun to_string/2}
),
unicode:characters_to_binary(String).
render_urlencoded_str(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_template:render(
Template,
rename_client_info_vars(Credential),
#{var_trans => fun to_urlencoded_string/2}
),
unicode:characters_to_binary(String).
render_sql_params(ParamList, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Row, _Errors} = emqx_template:render(
ParamList,
rename_client_info_vars(Credential),
#{var_trans => fun to_sql_value/2}
),
Row.
to_urlencoded_string(Name, Value) ->
case uri_string:compose_query([{<<"q">>, to_string(Name, Value)}]) of
<<"q=", EncodedBin/binary>> ->
EncodedBin;
"q=" ++ EncodedStr ->
list_to_binary(EncodedStr)
end.
to_string(Name, Value) ->
emqx_template:to_string(render_var(Name, Value)).
%% This converter is to generate data structure possibly with non-utf8 strings.
%% It converts to unicode only strings (character lists).
to_string_for_raw(Name, Value) ->
strings_to_unicode(Name, render_var(Name, Value)).
%% This converter is to generate data structure suitable for JSON serialization.
%% JSON strings are sequences of unicode characters, not bytes.
%% So we force all rendered data to be unicode, not only character lists.
to_string_for_json(Name, Value) ->
all_to_unicode(Name, render_var(Name, Value)).
strings_to_unicode(_Name, Value) when is_binary(Value) ->
Value;
strings_to_unicode(Name, Value) when is_list(Value) ->
to_unicode_binary(Name, Value);
strings_to_unicode(_Name, Value) ->
emqx_template:to_string(Value).
all_to_unicode(Name, Value) when is_list(Value) orelse is_binary(Value) ->
to_unicode_binary(Name, Value);
all_to_unicode(_Name, Value) ->
emqx_template:to_string(Value).
to_unicode_binary(Name, Value) when is_list(Value) orelse is_binary(Value) ->
try unicode:characters_to_binary(Value) of
Encoded when is_binary(Encoded) ->
Encoded;
_ ->
error({encode_error, {non_unicode_data, Name}})
catch
error:badarg ->
error({encode_error, {non_unicode_data, Name}})
end.
to_sql_value(Name, Value) ->
emqx_utils_sql:to_sql_value(render_var(Name, Value)).
render_var(_, undefined) ->
% NOTE
% Any allowed but undefined binding will be replaced with empty string, even when
% rendering SQL values.
<<>>;
render_var(?VAR_PEERHOST, Value) ->
inet:ntoa(Value);
render_var(?VAR_PASSWORD, Value) ->
iolist_to_binary(Value);
render_var(_Name, Value) ->
Value.
rename_client_info_vars(ClientInfo) ->
Renames = [
{cn, cert_common_name},
{dn, cert_subject},
{protocol, proto_name}
],
lists:foldl(
fun({Old, New}, Acc) ->
emqx_utils_maps:rename(Old, New, Acc)
end,
ClientInfo,
Renames
).
%%--------------------------------------------------------------------
%% URL parsing
-spec parse_url(binary()) -> -spec parse_url(binary()) ->
{_Base :: emqx_utils_uri:request_base(), _Path :: binary(), _Query :: binary()}. {_Base :: emqx_utils_uri:request_base(), _Path :: binary(), _Query :: binary()}.
parse_url(Url) -> parse_url(Url) ->
@ -48,6 +254,55 @@ parse_url(Url) ->
end end
end. end.
%%--------------------------------------------------------------------
%% HTTP request/response helpers
generate_request(
#{
method := Method,
headers := Headers,
base_path_template := BasePathTemplate,
base_query_template := BaseQueryTemplate,
body_template := BodyTemplate
},
Values
) ->
Path = render_urlencoded_str(BasePathTemplate, Values),
Query = render_deep_for_url(BaseQueryTemplate, Values),
case Method of
get ->
Body = render_deep_for_url(BodyTemplate, Values),
NPath = append_query(Path, Query, Body),
{ok, {NPath, Headers}};
_ ->
try
ContentType = post_request_content_type(Headers),
Body = serialize_body(ContentType, BodyTemplate, Values),
NPathQuery = append_query(Path, Query),
{ok, {NPathQuery, Headers, Body}}
catch
error:{encode_error, _} = Reason ->
{error, Reason}
end
end.
post_request_content_type(Headers) ->
proplists:get_value(<<"content-type">>, Headers, ?DEFAULT_HTTP_REQUEST_CONTENT_TYPE).
append_query(Path, []) ->
Path;
append_query(Path, Query) ->
[Path, $?, uri_string:compose_query(Query)].
append_query(Path, Query, Body) ->
append_query(Path, Query ++ maps:to_list(Body)).
serialize_body(<<"application/json">>, BodyTemplate, ClientInfo) ->
Body = emqx_auth_utils:render_deep_for_json(BodyTemplate, ClientInfo),
emqx_utils_json:encode(Body);
serialize_body(<<"application/x-www-form-urlencoded">>, BodyTemplate, ClientInfo) ->
Body = emqx_auth_utils:render_deep_for_url(BodyTemplate, ClientInfo),
uri_string:compose_query(maps:to_list(Body)).
-ifdef(TEST). -ifdef(TEST).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").

View File

@ -16,8 +16,8 @@
-module(emqx_authn_utils). -module(emqx_authn_utils).
-include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("emqx_authn.hrl"). -include_lib("emqx_authn.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("snabbkaffe/include/trace.hrl"). -include_lib("snabbkaffe/include/trace.hrl").
-export([ -export([
@ -26,13 +26,7 @@
check_password_from_selected_map/3, check_password_from_selected_map/3,
parse_deep/1, parse_deep/1,
parse_str/1, parse_str/1,
parse_str/2,
parse_sql/2, parse_sql/2,
render_deep_for_json/2,
render_deep_for_url/2,
render_str/2,
render_urlencoded_str/2,
render_sql_params/2,
is_superuser/1, is_superuser/1,
client_attrs/1, client_attrs/1,
bin/1, bin/1,
@ -47,18 +41,6 @@
default_headers_no_content_type/0 default_headers_no_content_type/0
]). ]).
%% VAR_NS_CLIENT_ATTRS is added here because it can be initialized before authn.
%% NOTE: authn return may add more to (or even overwrite) client_attrs.
-define(ALLOWED_VARS, [
?VAR_USERNAME,
?VAR_CLIENTID,
?VAR_PASSWORD,
?VAR_PEERHOST,
?VAR_CERT_SUBJECT,
?VAR_CERT_CN_NAME,
?VAR_NS_CLIENT_ATTRS
]).
-define(DEFAULT_RESOURCE_OPTS, #{ -define(DEFAULT_RESOURCE_OPTS, #{
start_after_created => false start_after_created => false
}). }).
@ -89,6 +71,13 @@ start_resource_if_enabled({ok, _} = Result, ResourceId, #{enable := true}) ->
start_resource_if_enabled(Result, _ResourceId, _Config) -> start_resource_if_enabled(Result, _ResourceId, _Config) ->
Result. Result.
parse_deep(Template) -> emqx_auth_utils:parse_deep(Template, ?AUTHN_DEFAULT_ALLOWED_VARS).
parse_str(Template) -> emqx_auth_utils:parse_str(Template, ?AUTHN_DEFAULT_ALLOWED_VARS).
parse_sql(Template, ReplaceWith) ->
emqx_auth_utils:parse_sql(Template, ReplaceWith, ?AUTHN_DEFAULT_ALLOWED_VARS).
check_password_from_selected_map(_Algorithm, _Selected, undefined) -> check_password_from_selected_map(_Algorithm, _Selected, undefined) ->
{error, bad_username_or_password}; {error, bad_username_or_password};
check_password_from_selected_map(Algorithm, Selected, Password) -> check_password_from_selected_map(Algorithm, Selected, Password) ->
@ -112,111 +101,6 @@ check_password_from_selected_map(Algorithm, Selected, Password) ->
end end
end. end.
parse_deep(Template) ->
Result = emqx_template:parse_deep(Template),
handle_disallowed_placeholders(Result, ?ALLOWED_VARS, {deep, Template}).
parse_str(Template, AllowedVars) ->
Result = emqx_template:parse(Template),
handle_disallowed_placeholders(Result, AllowedVars, {string, Template}).
parse_str(Template) ->
parse_str(Template, ?ALLOWED_VARS).
parse_sql(Template, ReplaceWith) ->
{Statement, Result} = emqx_template_sql:parse_prepstmt(
Template,
#{parameters => ReplaceWith, strip_double_quote => true}
),
{Statement, handle_disallowed_placeholders(Result, ?ALLOWED_VARS, {string, Template})}.
handle_disallowed_placeholders(Template, AllowedVars, Source) ->
case emqx_template:validate(AllowedVars, Template) of
ok ->
Template;
{error, Disallowed} ->
?tp(warning, "authn_template_invalid", #{
template => Source,
reason => Disallowed,
allowed => #{placeholders => AllowedVars},
notice =>
"Disallowed placeholders will be rendered as is."
" However, consider using `${$}` escaping for literal `$` where"
" needed to avoid unexpected results."
}),
Result = prerender_disallowed_placeholders(Template),
case Source of
{string, _} ->
emqx_template:parse(Result);
{deep, _} ->
emqx_template:parse_deep(Result)
end
end.
prerender_disallowed_placeholders(Template) ->
{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_VARS) of
true -> "${" ++ Name ++ "}";
false -> "${$}{" ++ Name ++ "}"
end
end
}),
Result.
render_deep_for_json(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Term, _Errors} = emqx_template:render(
Template,
mapping_credential(Credential),
#{var_trans => fun to_string_for_json/2}
),
Term.
render_deep_for_url(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Term, _Errors} = emqx_template:render(
Template,
mapping_credential(Credential),
#{var_trans => fun to_string_for_urlencode/2}
),
Term.
render_str(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_template:render(
Template,
mapping_credential(Credential),
#{var_trans => fun to_string/2}
),
unicode:characters_to_binary(String).
render_urlencoded_str(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_template:render(
Template,
mapping_credential(Credential),
#{var_trans => fun to_urlencoded_string/2}
),
unicode:characters_to_binary(String).
render_sql_params(ParamList, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Row, _Errors} = emqx_template:render(
ParamList,
mapping_credential(Credential),
#{var_trans => fun to_sql_value/2}
),
Row.
is_superuser(#{<<"is_superuser">> := Value}) -> is_superuser(#{<<"is_superuser">> := Value}) ->
#{is_superuser => to_bool(Value)}; #{is_superuser => to_bool(Value)};
is_superuser(#{}) -> is_superuser(#{}) ->
@ -338,63 +222,6 @@ without_password(Credential, [Name | Rest]) ->
without_password(Credential, Rest) without_password(Credential, Rest)
end. end.
to_urlencoded_string(Name, Value) ->
<<"q=", EncodedValue/binary>> = uri_string:compose_query([{<<"q">>, to_string(Name, Value)}]),
EncodedValue.
to_string(Name, Value) ->
emqx_template:to_string(render_var(Name, Value)).
%% Any data may be urlencoded, so we allow non-unicode binaries here.
to_string_for_urlencode(Name, Value) ->
to_string_for_urlencode(render_var(Name, Value)).
to_string_for_urlencode(Value) when is_binary(Value) ->
Value;
to_string_for_urlencode(Value) when is_list(Value) ->
unicode:characters_to_binary(Value);
to_string_for_urlencode(Value) ->
emqx_template:to_string(Value).
%% JSON strings are sequences of unicode characters, not bytes.
%% So we force all rendered data to be unicode.
to_string_for_json(Name, Value) ->
to_unicode_string(Name, render_var(Name, Value)).
to_unicode_string(Name, Value) when is_list(Value) orelse is_binary(Value) ->
try unicode:characters_to_binary(Value) of
Encoded when is_binary(Encoded) ->
Encoded;
_ ->
error({encode_error, {non_unicode_data, Name}})
catch error:badarg ->
error({encode_error, {non_unicode_data, Name}})
end;
to_unicode_string(_Name, Value) ->
emqx_template:to_string(Value).
to_sql_value(Name, Value) ->
emqx_utils_sql:to_sql_value(render_var(Name, Value)).
render_var(_, undefined) ->
% NOTE
% Any allowed but undefined binding will be replaced with empty string, even when
% rendering SQL values.
<<>>;
render_var(?VAR_PEERHOST, Value) ->
inet:ntoa(Value);
render_var(?VAR_PASSWORD, Value) ->
iolist_to_binary(Value);
render_var(_Name, Value) ->
Value.
mapping_credential(C = #{cn := CN, dn := DN}) ->
C#{cert_common_name => CN, cert_subject => DN};
mapping_credential(C) ->
C.
transform_header_name(Headers) -> transform_header_name(Headers) ->
maps:fold( maps:fold(
fun(K0, V, Acc) -> fun(K0, V, Acc) ->

View File

@ -259,7 +259,7 @@ compile_topic(<<"eq ", Topic/binary>>) ->
compile_topic({eq, Topic}) -> compile_topic({eq, Topic}) ->
{eq, emqx_topic:words(bin(Topic))}; {eq, emqx_topic:words(bin(Topic))};
compile_topic(Topic) -> compile_topic(Topic) ->
Template = emqx_authz_utils:parse_str(Topic, ?ALLOWED_VARS), Template = emqx_auth_utils:parse_str(Topic, ?ALLOWED_VARS),
case emqx_template:is_const(Template) of case emqx_template:is_const(Template) of
true -> emqx_topic:words(bin(Topic)); true -> emqx_topic:words(bin(Topic));
false -> {pattern, Template} false -> {pattern, Template}

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").
-include_lib("snabbkaffe/include/trace.hrl"). -include_lib("snabbkaffe/include/trace.hrl").
@ -28,14 +27,6 @@
update_resource/2, update_resource/2,
remove_resource/1, remove_resource/1,
update_config/2, update_config/2,
parse_deep/2,
parse_str/2,
render_urlencoded_str/2,
parse_sql/3,
render_deep/2,
render_str/2,
render_sql_params/2,
client_vars/1,
vars_for_rule_query/2, vars_for_rule_query/2,
parse_rule_from_row/2 parse_rule_from_row/2
]). ]).
@ -100,7 +91,7 @@ cleanup_resources() ->
). ).
make_resource_id(Name) -> make_resource_id(Name) ->
NameBin = bin(Name), NameBin = emqx_utils_conv:bin(Name),
emqx_resource:generate_id(NameBin). emqx_resource:generate_id(NameBin).
update_config(Path, ConfigRequest) -> update_config(Path, ConfigRequest) ->
@ -109,85 +100,6 @@ update_config(Path, ConfigRequest) ->
override_to => cluster override_to => cluster
}). }).
parse_deep(Template, PlaceHolders) ->
Result = emqx_template:parse_deep(Template),
handle_disallowed_placeholders(Result, {deep, Template}, PlaceHolders).
parse_str(Template, PlaceHolders) ->
Result = emqx_template:parse(Template),
handle_disallowed_placeholders(Result, {string, Template}, PlaceHolders).
parse_sql(Template, ReplaceWith, PlaceHolders) ->
{Statement, Result} = emqx_template_sql:parse_prepstmt(
Template,
#{parameters => ReplaceWith, strip_double_quote => true}
),
FResult = handle_disallowed_placeholders(Result, {string, Template}, PlaceHolders),
{Statement, FResult}.
handle_disallowed_placeholders(Template, Source, Allowed) ->
case emqx_template:validate(Allowed, Template) of
ok ->
Template;
{error, Disallowed} ->
?tp(warning, "authz_template_invalid", #{
template => Source,
reason => Disallowed,
allowed => #{placeholders => Allowed},
notice =>
"Disallowed placeholders will be rendered as is."
" However, consider using `${$}` escaping for literal `$` where"
" needed to avoid unexpected results."
}),
Result = emqx_template:escape_disallowed(Template, Allowed),
case Source of
{string, _} ->
emqx_template:parse(Result);
{deep, _} ->
emqx_template:parse_deep(Result)
end
end.
render_deep(Template, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Term, _Errors} = emqx_template:render(
Template,
client_vars(Values),
#{var_trans => fun to_string/2}
),
Term.
render_str(Template, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_template:render(
Template,
client_vars(Values),
#{var_trans => fun to_string/2}
),
unicode:characters_to_binary(String).
render_urlencoded_str(Template, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_template:render(
Template,
client_vars(Values),
#{var_trans => fun to_urlencoded_string/2}
),
unicode:characters_to_binary(String).
render_sql_params(ParamList, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Row, _Errors} = emqx_template:render(
ParamList,
client_vars(Values),
#{var_trans => fun to_sql_value/2}
),
Row.
-spec parse_http_resp_body(binary(), binary()) -> allow | deny | ignore | error. -spec parse_http_resp_body(binary(), binary()) -> allow | deny | ignore | error.
parse_http_resp_body(<<"application/x-www-form-urlencoded", _/binary>>, Body) -> parse_http_resp_body(<<"application/x-www-form-urlencoded", _/binary>>, Body) ->
try try
@ -239,42 +151,6 @@ vars_for_rule_query(Client, ?authz_action(PubSub, Qos) = Action) ->
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
client_vars(ClientInfo) ->
maps:from_list(
lists:map(
fun convert_client_var/1,
maps:to_list(ClientInfo)
)
).
convert_client_var({cn, CN}) -> {cert_common_name, CN};
convert_client_var({dn, DN}) -> {cert_subject, DN};
convert_client_var({protocol, Proto}) -> {proto_name, Proto};
convert_client_var(Other) -> Other.
to_urlencoded_string(Name, Value) ->
emqx_http_lib:uri_encode(to_string(Name, Value)).
to_string(Name, Value) ->
emqx_template:to_string(render_var(Name, Value)).
to_sql_value(Name, Value) ->
emqx_utils_sql:to_sql_value(render_var(Name, Value)).
render_var(_, undefined) ->
% NOTE
% Any allowed but undefined binding will be replaced with empty string, even when
% rendering SQL values.
<<>>;
render_var(?VAR_PEERHOST, Value) ->
inet:ntoa(Value);
render_var(_Name, Value) ->
Value.
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(L) when is_list(L) -> list_to_binary(L);
bin(X) -> X.
to_list(Tuple) when is_tuple(Tuple) -> to_list(Tuple) when is_tuple(Tuple) ->
tuple_to_list(Tuple); tuple_to_list(Tuple);
to_list(List) when is_list(List) -> to_list(List) when is_list(List) ->

View File

@ -156,12 +156,11 @@ parse_config(
request_timeout := RequestTimeout request_timeout := RequestTimeout
} = Config } = Config
) -> ) ->
ct:print("parse_config: ~p~n", [Config]),
{RequestBase, Path, Query} = emqx_auth_utils:parse_url(RawUrl), {RequestBase, Path, Query} = emqx_auth_utils:parse_url(RawUrl),
State = #{ State = #{
method => Method, method => Method,
path => Path, path => Path,
headers => Headers, headers => maps:to_list(Headers),
base_path_template => emqx_authn_utils:parse_str(Path), base_path_template => emqx_authn_utils:parse_str(Path),
base_query_template => emqx_authn_utils:parse_deep( base_query_template => emqx_authn_utils:parse_deep(
cow_qs:parse_qs(Query) cow_qs:parse_qs(Query)
@ -180,48 +179,8 @@ parse_config(
}, },
State}. State}.
generate_request(Credential, #{ generate_request(Credential, State) ->
method := Method, emqx_auth_utils:generate_request(State, Credential).
headers := Headers0,
base_path_template := BasePathTemplate,
base_query_template := BaseQueryTemplate,
body_template := BodyTemplate
}) ->
Headers = maps:to_list(Headers0),
Path = emqx_authn_utils:render_urlencoded_str(BasePathTemplate, Credential),
Query = emqx_authn_utils:render_deep_for_url(BaseQueryTemplate, Credential),
case Method of
get ->
Body = emqx_authn_utils:render_deep_for_url(BodyTemplate, Credential),
NPathQuery = append_query(to_list(Path), to_list(Query) ++ maps:to_list(Body)),
{ok, {NPathQuery, Headers}};
post ->
ContentType = post_request_content_type(Headers),
try
Body = serialize_body(ContentType, BodyTemplate, Credential),
NPathQuery = append_query(to_list(Path), to_list(Query)),
{ok, {NPathQuery, Headers, Body}}
catch
error:{encode_error, _} = Reason ->
{error, Reason}
end
end.
append_query(Path, []) ->
Path;
append_query(Path, Query) ->
ct:print("append_query: ~p~n", [Query]),
Path ++ "?" ++ qs(Query).
qs(KVs) ->
uri_string:compose_query(KVs).
serialize_body(<<"application/json">>, BodyTemplate, Credential) ->
Body = emqx_authn_utils:render_deep_for_json(BodyTemplate, Credential),
emqx_utils_json:encode(Body);
serialize_body(<<"application/x-www-form-urlencoded">>, BodyTemplate, Credential) ->
Body = emqx_authn_utils:render_deep_for_url(BodyTemplate, Credential),
qs(maps:to_list(Body)).
handle_response(Headers, Body) -> handle_response(Headers, Body) ->
ContentType = proplists:get_value(<<"content-type">>, Headers), ContentType = proplists:get_value(<<"content-type">>, Headers),
@ -267,26 +226,31 @@ parse_body(<<"application/x-www-form-urlencoded", _/binary>>, Body) ->
parse_body(ContentType, _) -> parse_body(ContentType, _) ->
{error, {unsupported_content_type, ContentType}}. {error, {unsupported_content_type, ContentType}}.
post_request_content_type(Headers) ->
proplists:get_value(<<"content-type">>, Headers, ?DEFAULT_CONTENT_TYPE).
request_for_log(Credential, #{url := Url, method := Method} = State) -> request_for_log(Credential, #{url := Url, method := Method} = 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
{PathQuery, Headers} -> {ok, {PathQuery, Headers}} ->
#{ #{
method => Method, method => Method,
url => Url, url => Url,
path_query => PathQuery, path_query => PathQuery,
headers => Headers headers => Headers
}; };
{PathQuery, Headers, Body} -> {ok, {PathQuery, Headers, Body}} ->
#{ #{
method => Method, method => Method,
url => Url, url => Url,
path_query => PathQuery, path_query => PathQuery,
headers => Headers, headers => Headers,
body => Body body => Body
};
%% we can't get here actually because the real request was already generated
%% successfully, so generating it with hidden password won't fail either.
{error, Reason} ->
#{
method => Method,
url => Url,
error => Reason
} }
end. end.
@ -297,20 +261,5 @@ response_for_log({ok, StatusCode, Headers, Body}) ->
response_for_log({error, Error}) -> response_for_log({error, Error}) ->
#{error => Error}. #{error => Error}.
to_list(A) when is_atom(A) ->
atom_to_list(A);
to_list(B) when is_binary(B) ->
binary_to_list(B);
to_list(L) when is_list(L) ->
L.
ensure_binary_names(Headers) -> ensure_binary_names(Headers) ->
Fun = fun emqx_utils_maps:binary_key_map(Headers).
(Key, _Val, Acc) when is_binary(Key) ->
Acc;
(Key, Val, Acc) when is_atom(Key) ->
Acc2 = maps:remove(Key, Acc),
BinKey = erlang:atom_to_binary(Key),
Acc2#{BinKey => Val}
end,
maps:fold(Fun, Headers, Headers).

View File

@ -85,34 +85,42 @@ authorize(
request_timeout := RequestTimeout request_timeout := RequestTimeout
} = Config } = Config
) -> ) ->
Request = generate_request(Action, Topic, Client, Config), case generate_request(Action, Topic, Client, Config) of
case emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of {ok, Request} ->
{ok, 204, _Headers} -> case emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of
{matched, allow}; {ok, 204, _Headers} ->
{ok, 200, Headers, Body} -> {matched, allow};
ContentType = emqx_authz_utils:content_type(Headers), {ok, 200, Headers, Body} ->
case emqx_authz_utils:parse_http_resp_body(ContentType, Body) of ContentType = emqx_authz_utils:content_type(Headers),
error -> case emqx_authz_utils:parse_http_resp_body(ContentType, Body) of
?SLOG(error, #{ error ->
msg => authz_http_response_incorrect, ?SLOG(error, #{
content_type => ContentType, msg => authz_http_response_incorrect,
body => Body content_type => ContentType,
}), body => Body
}),
nomatch;
Result ->
{matched, Result}
end;
{ok, Status, Headers} ->
log_nomtach_msg(Status, Headers, undefined),
nomatch; nomatch;
Result -> {ok, Status, Headers, Body} ->
{matched, Result} log_nomtach_msg(Status, Headers, Body),
nomatch;
{error, Reason} ->
?tp(authz_http_request_failure, #{error => Reason}),
?SLOG(error, #{
msg => "http_server_query_failed",
resource => ResourceID,
reason => Reason
}),
ignore
end; end;
{ok, Status, Headers} ->
log_nomtach_msg(Status, Headers, undefined),
nomatch;
{ok, Status, Headers, Body} ->
log_nomtach_msg(Status, Headers, Body),
nomatch;
{error, Reason} -> {error, Reason} ->
?tp(authz_http_request_failure, #{error => Reason}),
?SLOG(error, #{ ?SLOG(error, #{
msg => "http_server_query_failed", msg => "http_request_generation_failed",
resource => ResourceID,
reason => Reason reason => Reason
}), }),
ignore ignore
@ -156,86 +164,29 @@ parse_config(
method => Method, method => Method,
request_base => RequestBase, request_base => RequestBase,
headers => Headers, headers => Headers,
base_path_template => emqx_authz_utils:parse_str(Path, allowed_vars()), base_path_template => emqx_auth_utils:parse_str(Path, allowed_vars()),
base_query_template => emqx_authz_utils:parse_deep( base_query_template => emqx_auth_utils:parse_deep(
cow_qs:parse_qs(Query), cow_qs:parse_qs(Query),
allowed_vars() allowed_vars()
), ),
body_template => emqx_authz_utils:parse_deep( body_template =>
maps:to_list(maps:get(body, Conf, #{})), emqx_auth_utils:parse_deep(
allowed_vars() emqx_utils_maps:binary_key_map(maps:get(body, Conf, #{})),
), allowed_vars()
),
request_timeout => ReqTimeout, request_timeout => ReqTimeout,
%% pool_type default value `random` %% pool_type default value `random`
pool_type => random pool_type => random
}. }.
generate_request( generate_request(Action, Topic, Client, Config) ->
Action,
Topic,
Client,
#{
method := Method,
headers := Headers,
base_path_template := BasePathTemplate,
base_query_template := BaseQueryTemplate,
body_template := BodyTemplate
}
) ->
Values = client_vars(Client, Action, Topic), Values = client_vars(Client, Action, Topic),
Path = emqx_authz_utils:render_urlencoded_str(BasePathTemplate, Values), emqx_auth_utils:generate_request(Config, Values).
Query = emqx_authz_utils:render_deep(BaseQueryTemplate, Values),
Body = emqx_authz_utils:render_deep(BodyTemplate, Values),
case Method of
get ->
NPath = append_query(Path, Query ++ Body),
{NPath, Headers};
_ ->
NPath = append_query(Path, Query),
NBody = serialize_body(
proplists:get_value(<<"accept">>, Headers, <<"application/json">>),
Body
),
{NPath, Headers, NBody}
end.
append_query(Path, []) ->
to_list(Path);
append_query(Path, Query) ->
to_list(Path) ++ "?" ++ to_list(query_string(Query)).
query_string(Body) ->
query_string(Body, []).
query_string([], Acc) ->
case iolist_to_binary(lists:reverse(Acc)) of
<<$&, Str/binary>> ->
Str;
<<>> ->
<<>>
end;
query_string([{K, V} | More], Acc) ->
query_string(More, [["&", uri_encode(K), "=", uri_encode(V)] | Acc]).
uri_encode(T) ->
emqx_http_lib:uri_encode(to_list(T)).
serialize_body(<<"application/json">>, Body) ->
emqx_utils_json:encode(Body);
serialize_body(<<"application/x-www-form-urlencoded">>, Body) ->
query_string(Body).
client_vars(Client, Action, Topic) -> client_vars(Client, Action, Topic) ->
Vars = emqx_authz_utils:vars_for_rule_query(Client, Action), Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
Vars#{topic => Topic}. Vars#{topic => Topic}.
to_list(A) when is_atom(A) ->
atom_to_list(A);
to_list(B) when is_binary(B) ->
binary_to_list(B);
to_list(L) when is_list(L) ->
L.
allowed_vars() -> allowed_vars() ->
allowed_vars(emqx_authz:feature_available(rich_actions)). allowed_vars(emqx_authz:feature_available(rich_actions)).

View File

@ -140,6 +140,7 @@ t_create_invalid(_Config) ->
). ).
t_authenticate(_Config) -> t_authenticate(_Config) ->
ok = emqx_logger:set_primary_log_level(debug),
ok = lists:foreach( ok = lists:foreach(
fun(Sample) -> fun(Sample) ->
ct:pal("test_user_auth sample: ~p", [Sample]), ct:pal("test_user_auth sample: ~p", [Sample]),
@ -148,11 +149,13 @@ t_authenticate(_Config) ->
samples() samples()
). ).
test_user_auth(#{ test_user_auth(
handler := Handler, #{
config_params := SpecificConfgParams, handler := Handler,
result := Expect config_params := SpecificConfgParams,
} = Sample) -> result := Expect
} = Sample
) ->
Credentials = maps:merge(?CREDENTIALS, maps:get(credentials, Sample, #{})), Credentials = maps:merge(?CREDENTIALS, maps:get(credentials, Sample, #{})),
Result = perform_user_auth(SpecificConfgParams, Handler, Credentials), Result = perform_user_auth(SpecificConfgParams, Handler, Credentials),
?assertEqual(Expect, Result). ?assertEqual(Expect, Result).
@ -657,7 +660,6 @@ samples() ->
<<"username">> := <<"plain">>, <<"username">> := <<"plain">>,
<<"password">> := <<"plain">> <<"password">> := <<"plain">>
} = emqx_utils_json:decode(RawBody, [return_maps]), } = emqx_utils_json:decode(RawBody, [return_maps]),
ct:print("headers: ~p", [cowboy_req:headers(Req0)]),
<<"application/json">> = cowboy_req:header(<<"content-type">>, Req0), <<"application/json">> = cowboy_req:header(<<"content-type">>, Req0),
Req = cowboy_req:reply( Req = cowboy_req:reply(
200, 200,

View File

@ -253,9 +253,9 @@ t_path(_Config) ->
fun(Req0, State) -> fun(Req0, State) ->
?assertEqual( ?assertEqual(
<< <<
"/authz/use%20rs/" "/authz/use+rs/"
"user%20name/" "user+name/"
"client%20id/" "client+id/"
"127.0.0.1/" "127.0.0.1/"
"MQTT/" "MQTT/"
"MOUNTPOINT/" "MOUNTPOINT/"
@ -270,7 +270,7 @@ t_path(_Config) ->
end, end,
#{ #{
<<"url">> => << <<"url">> => <<
"http://127.0.0.1:33333/authz/use%20rs/" "http://127.0.0.1:33333/authz/use+rs/"
"${username}/" "${username}/"
"${clientid}/" "${clientid}/"
"${peerhost}/" "${peerhost}/"
@ -402,7 +402,7 @@ t_placeholder_and_body(_Config) ->
cowboy_req:path(Req0) cowboy_req:path(Req0)
), ),
{ok, [{PostVars, true}], Req1} = cowboy_req:read_urlencoded_body(Req0), {ok, PostVars, Req1} = cowboy_req:read_urlencoded_body(Req0),
?assertMatch( ?assertMatch(
#{ #{
@ -416,7 +416,7 @@ t_placeholder_and_body(_Config) ->
<<"CN">> := ?PH_CERT_CN_NAME, <<"CN">> := ?PH_CERT_CN_NAME,
<<"CS">> := ?PH_CERT_SUBJECT <<"CS">> := ?PH_CERT_SUBJECT
}, },
emqx_utils_json:decode(PostVars, [return_maps]) maps:from_list(PostVars)
), ),
{ok, ?AUTHZ_HTTP_RESP(allow, Req1), State} {ok, ?AUTHZ_HTTP_RESP(allow, Req1), State}
end, end,
@ -536,7 +536,7 @@ t_disallowed_placeholders_path(_Config) ->
{ok, ?AUTHZ_HTTP_RESP(allow, Req), State} {ok, ?AUTHZ_HTTP_RESP(allow, Req), State}
end, end,
#{ #{
<<"url">> => <<"http://127.0.0.1:33333/authz/use%20rs/${typo}">> <<"url">> => <<"http://127.0.0.1:33333/authz/use+rs/${typo}">>
} }
), ),

View File

@ -216,7 +216,7 @@ may_decode_secret(true, Secret) ->
render_expected([], _Variables) -> render_expected([], _Variables) ->
[]; [];
render_expected([{Name, ExpectedTemplate} | More], Variables) -> render_expected([{Name, ExpectedTemplate} | More], Variables) ->
Expected = emqx_authn_utils:render_str(ExpectedTemplate, Variables), Expected = emqx_auth_utils:render_str(ExpectedTemplate, Variables),
[{Name, Expected} | render_expected(More, Variables)]. [{Name, Expected} | render_expected(More, Variables)].
verify(undefined, _, _, _, _) -> verify(undefined, _, _, _, _) ->
@ -364,7 +364,7 @@ handle_verify_claims(VerifyClaims) ->
handle_verify_claims([], Acc) -> handle_verify_claims([], Acc) ->
Acc; Acc;
handle_verify_claims([{Name, Expected0} | More], Acc) -> handle_verify_claims([{Name, Expected0} | More], Acc) ->
Expected1 = emqx_authn_utils:parse_str(Expected0, ?ALLOWED_VARS), Expected1 = emqx_auth_utils:parse_str(Expected0, ?ALLOWED_VARS),
handle_verify_claims(More, [{Name, Expected1} | Acc]). handle_verify_claims(More, [{Name, Expected1} | Acc]).
binary_to_number(Bin) -> binary_to_number(Bin) ->

View File

@ -61,14 +61,27 @@ authenticate(#{auth_method := _}, _) ->
authenticate(#{password := undefined}, _) -> authenticate(#{password := undefined}, _) ->
{error, bad_username_or_password}; {error, bad_username_or_password};
authenticate( authenticate(
#{password := Password} = Credential, Credential, #{filter_template := FilterTemplate} = State
) ->
try emqx_auth_utils:render_deep_for_json(FilterTemplate, Credential) of
Filter ->
authenticate_with_filter(Filter, Credential, State)
catch
error:{encode_error, _} = EncodeError ->
?TRACE_AUTHN_PROVIDER(error, "mongodb_render_filter_failed", #{
reason => EncodeError
}),
ignore
end.
authenticate_with_filter(
Filter,
#{password := Password},
#{ #{
collection := Collection, collection := Collection,
filter_template := FilterTemplate,
resource_id := ResourceId resource_id := ResourceId
} = State } = State
) -> ) ->
Filter = emqx_authn_utils:render_deep_for_json(FilterTemplate, Credential),
case emqx_resource:simple_sync_query(ResourceId, {find_one, Collection, Filter, #{}}) of case emqx_resource:simple_sync_query(ResourceId, {find_one, Collection, Filter, #{}}) of
{ok, undefined} -> {ok, undefined} ->
ignore; ignore;

View File

@ -50,11 +50,11 @@ description() ->
create(#{filter := Filter} = Source) -> create(#{filter := Filter} = Source) ->
ResourceId = emqx_authz_utils:make_resource_id(?MODULE), ResourceId = emqx_authz_utils:make_resource_id(?MODULE),
{ok, _Data} = emqx_authz_utils:create_resource(ResourceId, emqx_mongodb, Source), {ok, _Data} = emqx_authz_utils:create_resource(ResourceId, emqx_mongodb, Source),
FilterTemp = emqx_authz_utils:parse_deep(Filter, ?ALLOWED_VARS), FilterTemp = emqx_auth_utils:parse_deep(Filter, ?ALLOWED_VARS),
Source#{annotations => #{id => ResourceId}, filter_template => FilterTemp}. Source#{annotations => #{id => ResourceId}, filter_template => FilterTemp}.
update(#{filter := Filter} = Source) -> update(#{filter := Filter} = Source) ->
FilterTemp = emqx_authz_utils:parse_deep(Filter, ?ALLOWED_VARS), FilterTemp = emqx_auth_utils:parse_deep(Filter, ?ALLOWED_VARS),
case emqx_authz_utils:update_resource(emqx_mongodb, Source) of case emqx_authz_utils:update_resource(emqx_mongodb, Source) of
{error, Reason} -> {error, Reason} ->
error({load_config_error, Reason}); error({load_config_error, Reason});
@ -69,13 +69,23 @@ authorize(
Client, Client,
Action, Action,
Topic, Topic,
#{ #{filter_template := FilterTemplate} = Config
collection := Collection,
filter_template := FilterTemplate,
annotations := #{id := ResourceID}
}
) -> ) ->
RenderedFilter = emqx_authz_utils:render_deep(FilterTemplate, Client), try emqx_auth_utils:render_deep_for_json(FilterTemplate, Client) of
RenderedFilter -> authorize_with_filter(RenderedFilter, Client, Action, Topic, Config)
catch
error:{encode_error, _} = EncodeError ->
?SLOG(error, #{
msg => "mongo_authorize_error",
reason => EncodeError
}),
nomatch
end.
authorize_with_filter(RenderedFilter, Client, Action, Topic, #{
collection := Collection,
annotations := #{id := ResourceID}
}) ->
case emqx_resource:simple_sync_query(ResourceID, {find, Collection, RenderedFilter, #{}}) of case emqx_resource:simple_sync_query(ResourceID, {find, Collection, RenderedFilter, #{}}) of
{error, Reason} -> {error, Reason} ->
?SLOG(error, #{ ?SLOG(error, #{

View File

@ -68,7 +68,7 @@ authenticate(
password_hash_algorithm := Algorithm password_hash_algorithm := Algorithm
} }
) -> ) ->
Params = emqx_authn_utils:render_sql_params(TmplToken, Credential), Params = emqx_auth_utils:render_sql_params(TmplToken, Credential),
case case
emqx_resource:simple_sync_query(ResourceId, {prepared_query, ?PREPARE_KEY, Params, Timeout}) emqx_resource:simple_sync_query(ResourceId, {prepared_query, ?PREPARE_KEY, Params, Timeout})
of of

View File

@ -50,14 +50,14 @@ description() ->
"AuthZ with Mysql". "AuthZ with Mysql".
create(#{query := SQL} = Source0) -> create(#{query := SQL} = Source0) ->
{PrepareSQL, TmplToken} = emqx_authz_utils:parse_sql(SQL, '?', ?ALLOWED_VARS), {PrepareSQL, TmplToken} = emqx_auth_utils:parse_sql(SQL, '?', ?ALLOWED_VARS),
ResourceId = emqx_authz_utils:make_resource_id(?MODULE), ResourceId = emqx_authz_utils:make_resource_id(?MODULE),
Source = Source0#{prepare_statement => #{?PREPARE_KEY => PrepareSQL}}, Source = Source0#{prepare_statement => #{?PREPARE_KEY => PrepareSQL}},
{ok, _Data} = emqx_authz_utils:create_resource(ResourceId, emqx_mysql, Source), {ok, _Data} = emqx_authz_utils:create_resource(ResourceId, emqx_mysql, Source),
Source#{annotations => #{id => ResourceId, tmpl_token => TmplToken}}. Source#{annotations => #{id => ResourceId, tmpl_token => TmplToken}}.
update(#{query := SQL} = Source0) -> update(#{query := SQL} = Source0) ->
{PrepareSQL, TmplToken} = emqx_authz_utils:parse_sql(SQL, '?', ?ALLOWED_VARS), {PrepareSQL, TmplToken} = emqx_auth_utils:parse_sql(SQL, '?', ?ALLOWED_VARS),
Source = Source0#{prepare_statement => #{?PREPARE_KEY => PrepareSQL}}, Source = Source0#{prepare_statement => #{?PREPARE_KEY => PrepareSQL}},
case emqx_authz_utils:update_resource(emqx_mysql, Source) of case emqx_authz_utils:update_resource(emqx_mysql, Source) of
{error, Reason} -> {error, Reason} ->
@ -81,7 +81,7 @@ authorize(
} }
) -> ) ->
Vars = emqx_authz_utils:vars_for_rule_query(Client, Action), Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
RenderParams = emqx_authz_utils:render_sql_params(TmplToken, Vars), RenderParams = emqx_auth_utils:render_sql_params(TmplToken, Vars),
case case
emqx_resource:simple_sync_query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams}) emqx_resource:simple_sync_query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams})
of of

View File

@ -76,7 +76,7 @@ authenticate(
password_hash_algorithm := Algorithm password_hash_algorithm := Algorithm
} }
) -> ) ->
Params = emqx_authn_utils:render_sql_params(PlaceHolders, Credential), Params = emqx_auth_utils:render_sql_params(PlaceHolders, Credential),
case emqx_resource:simple_sync_query(ResourceId, {prepared_query, ResourceId, Params}) of case emqx_resource:simple_sync_query(ResourceId, {prepared_query, ResourceId, Params}) of
{ok, _Columns, []} -> {ok, _Columns, []} ->
ignore; ignore;

View File

@ -50,7 +50,7 @@ description() ->
"AuthZ with PostgreSQL". "AuthZ with PostgreSQL".
create(#{query := SQL0} = Source) -> create(#{query := SQL0} = Source) ->
{SQL, PlaceHolders} = emqx_authz_utils:parse_sql(SQL0, '$n', ?ALLOWED_VARS), {SQL, PlaceHolders} = emqx_auth_utils:parse_sql(SQL0, '$n', ?ALLOWED_VARS),
ResourceID = emqx_authz_utils:make_resource_id(emqx_postgresql), ResourceID = emqx_authz_utils:make_resource_id(emqx_postgresql),
{ok, _Data} = emqx_authz_utils:create_resource( {ok, _Data} = emqx_authz_utils:create_resource(
ResourceID, ResourceID,
@ -60,7 +60,7 @@ create(#{query := SQL0} = Source) ->
Source#{annotations => #{id => ResourceID, placeholders => PlaceHolders}}. Source#{annotations => #{id => ResourceID, placeholders => PlaceHolders}}.
update(#{query := SQL0, annotations := #{id := ResourceID}} = Source) -> update(#{query := SQL0, annotations := #{id := ResourceID}} = Source) ->
{SQL, PlaceHolders} = emqx_authz_utils:parse_sql(SQL0, '$n', ?ALLOWED_VARS), {SQL, PlaceHolders} = emqx_auth_utils:parse_sql(SQL0, '$n', ?ALLOWED_VARS),
case case
emqx_authz_utils:update_resource( emqx_authz_utils:update_resource(
emqx_postgresql, emqx_postgresql,
@ -88,7 +88,7 @@ authorize(
} }
) -> ) ->
Vars = emqx_authz_utils:vars_for_rule_query(Client, Action), Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
RenderedParams = emqx_authz_utils:render_sql_params(Placeholders, Vars), RenderedParams = emqx_auth_utils:render_sql_params(Placeholders, Vars),
case case
emqx_resource:simple_sync_query(ResourceID, {prepared_query, ResourceID, RenderedParams}) emqx_resource:simple_sync_query(ResourceID, {prepared_query, ResourceID, RenderedParams})
of of

View File

@ -74,7 +74,7 @@ authenticate(
password_hash_algorithm := Algorithm password_hash_algorithm := Algorithm
} }
) -> ) ->
NKey = emqx_authn_utils:render_str(KeyTemplate, Credential), NKey = emqx_auth_utils:render_str(KeyTemplate, Credential),
Command = [CommandName, NKey | Fields], Command = [CommandName, NKey | Fields],
case emqx_resource:simple_sync_query(ResourceId, {cmd, Command}) of case emqx_resource:simple_sync_query(ResourceId, {cmd, Command}) of
{ok, []} -> {ok, []} ->

View File

@ -75,7 +75,7 @@ authorize(
} }
) -> ) ->
Vars = emqx_authz_utils:vars_for_rule_query(Client, Action), Vars = emqx_authz_utils:vars_for_rule_query(Client, Action),
Cmd = emqx_authz_utils:render_deep(CmdTemplate, Vars), Cmd = emqx_auth_utils:render_deep_for_raw(CmdTemplate, Vars),
case emqx_resource:simple_sync_query(ResourceID, {cmd, Cmd}) of case emqx_resource:simple_sync_query(ResourceID, {cmd, Cmd}) of
{ok, Rows} -> {ok, Rows} ->
do_authorize(Client, Action, Topic, Rows); do_authorize(Client, Action, Topic, Rows);
@ -134,7 +134,7 @@ parse_cmd(Query) ->
case emqx_redis_command:split(Query) of case emqx_redis_command:split(Query) of
{ok, Cmd} -> {ok, Cmd} ->
ok = validate_cmd(Cmd), ok = validate_cmd(Cmd),
emqx_authz_utils:parse_deep(Cmd, ?ALLOWED_VARS); emqx_auth_utils:parse_deep(Cmd, ?ALLOWED_VARS);
{error, Reason} -> {error, Reason} ->
error({invalid_redis_cmd, Reason, Query}) error({invalid_redis_cmd, Reason, Query})
end. end.

View File

@ -843,23 +843,43 @@ formalize_request(_Method, BasePath, {Path, Headers}) ->
%% %%
%% See also: `join_paths_test_/0` %% See also: `join_paths_test_/0`
join_paths(Path1, Path2) -> join_paths(Path1, Path2) ->
do_join_paths(lists:reverse(to_list(Path1)), to_list(Path2)). [without_trailing_slash(Path1), $/, without_starting_slash(Path2)].
%% "abc/" + "/cde" without_starting_slash(Path) ->
do_join_paths([$/ | Path1], [$/ | Path2]) -> case do_without_starting_slash(Path) of
lists:reverse(Path1) ++ [$/ | Path2]; empty -> <<>>;
%% "abc/" + "cde" Other -> Other
do_join_paths([$/ | Path1], Path2) -> end.
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; do_without_starting_slash([]) ->
to_list(Bin) when is_binary(Bin) -> binary_to_list(Bin). empty;
do_without_starting_slash(<<>>) ->
empty;
do_without_starting_slash([$/ | Rest]) ->
Rest;
do_without_starting_slash([C | _Rest] = Path) when is_integer(C) andalso C =/= $/ ->
Path;
do_without_starting_slash(<<$/, Rest/binary>>) ->
Rest;
do_without_starting_slash(<<C, _Rest/binary>> = Path) when is_integer(C) andalso C =/= $/ ->
Path;
%% On actual lists the recursion should very quickly exhaust
do_without_starting_slash([El | Rest]) ->
case do_without_starting_slash(El) of
empty -> do_without_starting_slash(Rest);
ElRest -> [ElRest | Rest]
end.
without_trailing_slash(Path) ->
case iolist_to_binary(Path) of
<<>> ->
<<>>;
B ->
case binary:last(B) of
$/ -> binary_part(B, 0, byte_size(B) - 1);
_ -> B
end
end.
to_bin(Bin) when is_binary(Bin) -> to_bin(Bin) when is_binary(Bin) ->
Bin; Bin;
@ -986,6 +1006,9 @@ clientid(Msg) -> maps:get(clientid, Msg, undefined).
-ifdef(TEST). -ifdef(TEST).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
iolists_equal(L1, L2) ->
iolist_to_binary(L1) =:= iolist_to_binary(L2).
redact_test_() -> redact_test_() ->
TestData = #{ TestData = #{
headers => [ headers => [
@ -999,19 +1022,57 @@ redact_test_() ->
join_paths_test_() -> join_paths_test_() ->
[ [
?_assertEqual("abc/cde", join_paths("abc", "cde")), ?_assert(iolists_equal("abc/cde", join_paths("abc", "cde"))),
?_assertEqual("abc/cde", join_paths("abc", "/cde")), ?_assert(iolists_equal("abc/cde", join_paths(<<"abc">>, <<"cde">>))),
?_assertEqual("abc/cde", join_paths("abc/", "cde")), ?_assert(
?_assertEqual("abc/cde", join_paths("abc/", "/cde")), iolists_equal(
"abc/cde",
join_paths([["a"], <<"b">>, <<"c">>], [
[[[], <<>>], <<>>, <<"c">>], <<"d">>, <<"e">>
])
)
),
?_assertEqual("/", join_paths("", "")), ?_assert(iolists_equal("abc/cde", join_paths("abc", "/cde"))),
?_assertEqual("/cde", join_paths("", "cde")), ?_assert(iolists_equal("abc/cde", join_paths(<<"abc">>, <<"/cde">>))),
?_assertEqual("/cde", join_paths("", "/cde")), ?_assert(
?_assertEqual("/cde", join_paths("/", "cde")), iolists_equal(
?_assertEqual("/cde", join_paths("/", "/cde")), "abc/cde",
join_paths([["a"], <<"b">>, <<"c">>], [
[<<>>, [[], <<>>], <<"/c">>], <<"d">>, <<"e">>
])
)
),
?_assertEqual("//cde/", join_paths("/", "//cde/")), ?_assert(iolists_equal("abc/cde", join_paths("abc/", "cde"))),
?_assertEqual("abc///cde/", join_paths("abc//", "//cde/")) ?_assert(iolists_equal("abc/cde", join_paths(<<"abc/">>, <<"cde">>))),
?_assert(
iolists_equal(
"abc/cde",
join_paths([["a"], <<"b">>, <<"c">>, [<<"/">>]], [
[[[], [], <<>>], <<>>, [], <<"c">>], <<"d">>, <<"e">>
])
)
),
?_assert(iolists_equal("abc/cde", join_paths("abc/", "/cde"))),
?_assert(iolists_equal("abc/cde", join_paths(<<"abc/">>, <<"/cde">>))),
?_assert(
iolists_equal(
"abc/cde",
join_paths([["a"], <<"b">>, <<"c">>, [<<"/">>]], [
[[[], <<>>], <<>>, [[$/]], <<"c">>], <<"d">>, <<"e">>
])
)
),
?_assert(iolists_equal("/", join_paths("", ""))),
?_assert(iolists_equal("/cde", join_paths("", "cde"))),
?_assert(iolists_equal("/cde", join_paths("", "/cde"))),
?_assert(iolists_equal("/cde", join_paths("/", "cde"))),
?_assert(iolists_equal("/cde", join_paths("/", "/cde"))),
?_assert(iolists_equal("//cde/", join_paths("/", "//cde/"))),
?_assert(iolists_equal("abc///cde/", join_paths("abc//", "//cde/")))
]. ].
-endif. -endif.

View File

@ -426,7 +426,6 @@ to_string(List) when is_list(List) ->
end. end.
character_segments_to_binary(StringSegments) -> character_segments_to_binary(StringSegments) ->
ct:print("characters_to_binary: ~p~n", [StringSegments]),
iolist_to_binary( iolist_to_binary(
lists:map( lists:map(
fun fun