chore(auth_http): unify http request generation
Co-authored-by: Thales Macedo Garitezi <thalesmg@gmail.com>
This commit is contained in:
parent
bca3782d73
commit
daf2e5a444
|
@ -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.
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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").
|
||||||
|
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
|
@ -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).
|
|
||||||
|
|
|
@ -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)).
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}">>
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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, #{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, []} ->
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue