Merge pull request #7817 from JimMoen/fix-auth-http
This commit is contained in:
commit
344a754674
|
@ -18,7 +18,7 @@ emqx_authn_http {
|
||||||
en: """HTTP request method."""
|
en: """HTTP request method."""
|
||||||
zh: """HTTP 请求方法。"""
|
zh: """HTTP 请求方法。"""
|
||||||
}
|
}
|
||||||
label: {
|
label {
|
||||||
en: """Request Method"""
|
en: """Request Method"""
|
||||||
zh: """请求方法"""
|
zh: """请求方法"""
|
||||||
}
|
}
|
||||||
|
@ -27,9 +27,9 @@ emqx_authn_http {
|
||||||
url {
|
url {
|
||||||
desc {
|
desc {
|
||||||
en: """URL of the HTTP server."""
|
en: """URL of the HTTP server."""
|
||||||
zh: """HTTP 服务器地址。"""
|
zh: """认证 HTTP 服务器地址。"""
|
||||||
}
|
}
|
||||||
label: {
|
label {
|
||||||
en: """URL"""
|
en: """URL"""
|
||||||
zh: """URL"""
|
zh: """URL"""
|
||||||
}
|
}
|
||||||
|
@ -37,23 +37,23 @@ emqx_authn_http {
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
desc {
|
desc {
|
||||||
en: """HTTP request headers."""
|
en: """List of HTTP Headers."""
|
||||||
zh: """HTTP request headers。"""
|
zh: """HTTP Headers 列表"""
|
||||||
}
|
}
|
||||||
label: {
|
label {
|
||||||
en: """Request Headers"""
|
en: """Headers"""
|
||||||
zh: """Request Headers"""
|
zh: """请求头"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers_no_content_type {
|
headers_no_content_type {
|
||||||
desc {
|
desc {
|
||||||
en: """HTTP request headers (without <code>content-type</code>)."""
|
en: """List of HTTP headers (without <code>content-type</code>)."""
|
||||||
zh: """HTTP request headers(无 <code>content-type</code>)。"""
|
zh: """HTTP Headers 列表 (无 <code>content-type</code>) 。"""
|
||||||
}
|
}
|
||||||
label: {
|
label {
|
||||||
en: """Request Headers"""
|
en: """headers_no_content_type"""
|
||||||
zh: """Request Headers"""
|
zh: """请求头(无 content-type)"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ emqx_authn_http {
|
||||||
en: """HTTP request body."""
|
en: """HTTP request body."""
|
||||||
zh: """HTTP request body。"""
|
zh: """HTTP request body。"""
|
||||||
}
|
}
|
||||||
label: {
|
label {
|
||||||
en: """Request Body"""
|
en: """Request Body"""
|
||||||
zh: """Request Body"""
|
zh: """Request Body"""
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ emqx_authn_http {
|
||||||
en: """HTTP request timeout."""
|
en: """HTTP request timeout."""
|
||||||
zh: """HTTP 请求超时时长。"""
|
zh: """HTTP 请求超时时长。"""
|
||||||
}
|
}
|
||||||
label: {
|
label {
|
||||||
en: """Request Timeout"""
|
en: """Request Timeout"""
|
||||||
zh: """请求超时时间"""
|
zh: """请求超时时间"""
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,10 @@ common_fields() ->
|
||||||
{mechanism, emqx_authn_schema:mechanism(password_based)},
|
{mechanism, emqx_authn_schema:mechanism(password_based)},
|
||||||
{backend, emqx_authn_schema:backend(http)},
|
{backend, emqx_authn_schema:backend(http)},
|
||||||
{url, fun url/1},
|
{url, fun url/1},
|
||||||
{body, hoconsc:mk(map([{fuzzy, term(), binary()}]), #{desc => ?DESC(body)})},
|
{body,
|
||||||
|
hoconsc:mk(map([{fuzzy, term(), binary()}]), #{
|
||||||
|
required => false, desc => ?DESC(body)
|
||||||
|
})},
|
||||||
{request_timeout, fun request_timeout/1}
|
{request_timeout, fun request_timeout/1}
|
||||||
] ++ emqx_authn_schema:common_fields() ++
|
] ++ emqx_authn_schema:common_fields() ++
|
||||||
maps:to_list(
|
maps:to_list(
|
||||||
|
@ -127,7 +130,10 @@ headers_no_content_type(desc) ->
|
||||||
?DESC(?FUNCTION_NAME);
|
?DESC(?FUNCTION_NAME);
|
||||||
headers_no_content_type(converter) ->
|
headers_no_content_type(converter) ->
|
||||||
fun(Headers) ->
|
fun(Headers) ->
|
||||||
|
maps:without(
|
||||||
|
[<<"content-type">>],
|
||||||
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
||||||
|
)
|
||||||
end;
|
end;
|
||||||
headers_no_content_type(default) ->
|
headers_no_content_type(default) ->
|
||||||
default_headers_no_content_type();
|
default_headers_no_content_type();
|
||||||
|
@ -155,26 +161,23 @@ create(_AuthenticatorID, Config) ->
|
||||||
create(
|
create(
|
||||||
#{
|
#{
|
||||||
method := Method,
|
method := Method,
|
||||||
url := RawURL,
|
url := RawUrl,
|
||||||
headers := HeadersT,
|
headers := Headers,
|
||||||
body := Body,
|
|
||||||
request_timeout := RequestTimeout
|
request_timeout := RequestTimeout
|
||||||
} = Config
|
} = Config
|
||||||
) ->
|
) ->
|
||||||
Headers = ensure_header_name_type(HeadersT),
|
{BaseUrl0, Path, Query} = parse_url(RawUrl),
|
||||||
{BsaeUrlWithPath, Query} = parse_fullpath(RawURL),
|
{ok, BaseUrl} = emqx_http_lib:uri_parse(BaseUrl0),
|
||||||
URIMap = parse_url(BsaeUrlWithPath),
|
|
||||||
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
||||||
State = #{
|
State = #{
|
||||||
method => Method,
|
method => Method,
|
||||||
path => maps:get(path, URIMap),
|
path => Path,
|
||||||
|
headers => Headers,
|
||||||
|
base_path_templete => 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(to_bin(Query))
|
cow_qs:parse_qs(to_bin(Query))
|
||||||
),
|
),
|
||||||
headers => maps:to_list(Headers),
|
body_template => emqx_authn_utils:parse_deep(maps:get(body, Config, #{})),
|
||||||
body_template => emqx_authn_utils:parse_deep(
|
|
||||||
maps:to_list(Body)
|
|
||||||
),
|
|
||||||
request_timeout => RequestTimeout,
|
request_timeout => RequestTimeout,
|
||||||
resource_id => ResourceId
|
resource_id => ResourceId
|
||||||
},
|
},
|
||||||
|
@ -184,7 +187,7 @@ create(
|
||||||
?RESOURCE_GROUP,
|
?RESOURCE_GROUP,
|
||||||
emqx_connector_http,
|
emqx_connector_http,
|
||||||
Config#{
|
Config#{
|
||||||
base_url => maps:remove(query, URIMap),
|
base_url => BaseUrl,
|
||||||
pool_type => random
|
pool_type => random
|
||||||
},
|
},
|
||||||
#{}
|
#{}
|
||||||
|
@ -273,9 +276,6 @@ destroy(#{resource_id := ResourceId}) ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
parse_fullpath(RawURL) ->
|
|
||||||
cow_http:parse_fullpath(to_bin(RawURL)).
|
|
||||||
|
|
||||||
default_headers() ->
|
default_headers() ->
|
||||||
maps:put(
|
maps:put(
|
||||||
<<"content-type">>,
|
<<"content-type">>,
|
||||||
|
@ -302,14 +302,14 @@ transform_header_name(Headers) ->
|
||||||
).
|
).
|
||||||
|
|
||||||
check_ssl_opts(Conf) ->
|
check_ssl_opts(Conf) ->
|
||||||
{BaseUrlWithPath, _Query} = parse_fullpath(get_conf_val("url", Conf)),
|
{BaseUrl, _Path, _Query} = parse_url(get_conf_val("url", Conf)),
|
||||||
case parse_url(BaseUrlWithPath) of
|
case BaseUrl of
|
||||||
#{scheme := https} ->
|
<<"https://", _/binary>> ->
|
||||||
case get_conf_val("ssl.enable", Conf) of
|
case get_conf_val("ssl.enable", Conf) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> false
|
false -> false
|
||||||
end;
|
end;
|
||||||
#{scheme := http} ->
|
<<"http://", _/binary>> ->
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -318,39 +318,51 @@ check_headers(Conf) ->
|
||||||
Headers = get_conf_val("headers", Conf),
|
Headers = get_conf_val("headers", Conf),
|
||||||
Method =:= <<"post">> orelse (not maps:is_key(<<"content-type">>, Headers)).
|
Method =:= <<"post">> orelse (not maps:is_key(<<"content-type">>, Headers)).
|
||||||
|
|
||||||
parse_url(URL) ->
|
parse_url(Url) ->
|
||||||
{ok, URIMap} = emqx_http_lib:uri_parse(URL),
|
case string:split(Url, "//", leading) of
|
||||||
case maps:get(query, URIMap, undefined) of
|
[Scheme, UrlRem] ->
|
||||||
undefined ->
|
case string:split(UrlRem, "/", leading) of
|
||||||
URIMap#{query => ""};
|
[HostPort, Remaining] ->
|
||||||
_ ->
|
BaseUrl = iolist_to_binary([Scheme, "//", HostPort]),
|
||||||
URIMap
|
case string:split(Remaining, "?", leading) of
|
||||||
|
[Path, QueryString] ->
|
||||||
|
{BaseUrl, Path, QueryString};
|
||||||
|
[Path] ->
|
||||||
|
{BaseUrl, Path, <<>>}
|
||||||
|
end;
|
||||||
|
[HostPort] ->
|
||||||
|
{iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>}
|
||||||
|
end;
|
||||||
|
[Url] ->
|
||||||
|
throw({invalid_url, Url})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
generate_request(Credential, #{
|
generate_request(Credential, #{
|
||||||
method := Method,
|
method := Method,
|
||||||
path := Path,
|
headers := Headers0,
|
||||||
|
base_path_templete := BasePathTemplate,
|
||||||
base_query_template := BaseQueryTemplate,
|
base_query_template := BaseQueryTemplate,
|
||||||
headers := Headers,
|
|
||||||
body_template := BodyTemplate
|
body_template := BodyTemplate
|
||||||
}) ->
|
}) ->
|
||||||
|
Headers = maps:to_list(Headers0),
|
||||||
|
Path = emqx_authn_utils:render_str(BasePathTemplate, Credential),
|
||||||
|
Query = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential),
|
||||||
Body = emqx_authn_utils:render_deep(BodyTemplate, Credential),
|
Body = emqx_authn_utils:render_deep(BodyTemplate, Credential),
|
||||||
NBaseQuery = emqx_authn_utils:render_deep(BaseQueryTemplate, Credential),
|
|
||||||
case Method of
|
case Method of
|
||||||
get ->
|
get ->
|
||||||
NPath = append_query(Path, NBaseQuery ++ Body),
|
NPathQuery = append_query(to_list(Path), to_list(Query) ++ maps:to_list(Body)),
|
||||||
{NPath, Headers};
|
{NPathQuery, Headers};
|
||||||
post ->
|
post ->
|
||||||
NPath = append_query(Path, NBaseQuery),
|
NPathQuery = append_query(to_list(Path), to_list(Query)),
|
||||||
ContentType = proplists:get_value(<<"content-type">>, Headers),
|
ContentType = proplists:get_value(<<"content-type">>, Headers),
|
||||||
NBody = serialize_body(ContentType, Body),
|
NBody = serialize_body(ContentType, Body),
|
||||||
{NPath, Headers, NBody}
|
{NPathQuery, Headers, NBody}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
append_query(Path, []) ->
|
append_query(Path, []) ->
|
||||||
Path;
|
encode_path(Path);
|
||||||
append_query(Path, Query) ->
|
append_query(Path, Query) ->
|
||||||
Path ++ "?" ++ binary_to_list(qs(Query)).
|
encode_path(Path) ++ "?" ++ binary_to_list(qs(Query)).
|
||||||
|
|
||||||
qs(KVs) ->
|
qs(KVs) ->
|
||||||
qs(KVs, []).
|
qs(KVs, []).
|
||||||
|
@ -387,12 +399,18 @@ may_append_body(Output, {ok, _, _}) ->
|
||||||
Output.
|
Output.
|
||||||
|
|
||||||
uri_encode(T) ->
|
uri_encode(T) ->
|
||||||
emqx_http_lib:uri_encode(to_bin(T)).
|
emqx_http_lib:uri_encode(to_list(T)).
|
||||||
|
|
||||||
|
encode_path(Path) ->
|
||||||
|
Parts = string:split(Path, "/", all),
|
||||||
|
lists:flatten(["/" ++ Part || Part <- lists:map(fun uri_encode/1, Parts)]).
|
||||||
|
|
||||||
to_list(A) when is_atom(A) ->
|
to_list(A) when is_atom(A) ->
|
||||||
atom_to_list(A);
|
atom_to_list(A);
|
||||||
to_list(B) when is_binary(B) ->
|
to_list(B) when is_binary(B) ->
|
||||||
binary_to_list(B).
|
binary_to_list(B);
|
||||||
|
to_list(L) when is_list(L) ->
|
||||||
|
L.
|
||||||
|
|
||||||
to_bin(A) when is_atom(A) ->
|
to_bin(A) when is_atom(A) ->
|
||||||
atom_to_binary(A);
|
atom_to_binary(A);
|
||||||
|
@ -403,14 +421,3 @@ to_bin(L) when is_list(L) ->
|
||||||
|
|
||||||
get_conf_val(Name, Conf) ->
|
get_conf_val(Name, Conf) ->
|
||||||
hocon_maps:get(?CONF_NS ++ "." ++ Name, Conf).
|
hocon_maps:get(?CONF_NS ++ "." ++ Name, Conf).
|
||||||
|
|
||||||
ensure_header_name_type(Headers) ->
|
|
||||||
Fun = fun
|
|
||||||
(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).
|
|
||||||
|
|
|
@ -184,14 +184,14 @@ test_authenticators(PathPrefix) ->
|
||||||
InvalidConfig0
|
InvalidConfig0
|
||||||
),
|
),
|
||||||
|
|
||||||
InvalidConfig1 = ValidConfig#{
|
ValidConfig1 = ValidConfig#{
|
||||||
method => <<"get">>,
|
method => <<"get">>,
|
||||||
headers => #{<<"content-type">> => <<"application/json">>}
|
headers => #{<<"content-type">> => <<"application/json">>}
|
||||||
},
|
},
|
||||||
{ok, 400, _} = request(
|
{ok, 200, _} = request(
|
||||||
post,
|
put,
|
||||||
uri(PathPrefix ++ [?CONF_NS]),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||||
InvalidConfig1
|
ValidConfig1
|
||||||
),
|
),
|
||||||
|
|
||||||
?assertAuthenticatorsMatch(
|
?assertAuthenticatorsMatch(
|
||||||
|
@ -264,23 +264,23 @@ test_authenticator(PathPrefix) ->
|
||||||
InvalidConfig0
|
InvalidConfig0
|
||||||
),
|
),
|
||||||
|
|
||||||
InvalidConfig1 = ValidConfig0#{
|
ValidConfig1 = ValidConfig0#{
|
||||||
method => <<"get">>,
|
method => <<"get">>,
|
||||||
headers => #{<<"content-type">> => <<"application/json">>}
|
headers => #{<<"content-type">> => <<"application/json">>}
|
||||||
},
|
},
|
||||||
{ok, 400, _} = request(
|
|
||||||
put,
|
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
|
||||||
InvalidConfig1
|
|
||||||
),
|
|
||||||
|
|
||||||
ValidConfig1 = ValidConfig0#{pool_size => 9},
|
|
||||||
{ok, 200, _} = request(
|
{ok, 200, _} = request(
|
||||||
put,
|
put,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||||
ValidConfig1
|
ValidConfig1
|
||||||
),
|
),
|
||||||
|
|
||||||
|
ValidConfig2 = ValidConfig0#{pool_size => 9},
|
||||||
|
{ok, 200, _} = request(
|
||||||
|
put,
|
||||||
|
uri(PathPrefix ++ [?CONF_NS, "password_based:http"]),
|
||||||
|
ValidConfig2
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 404, _} = request(
|
{ok, 404, _} = request(
|
||||||
delete,
|
delete,
|
||||||
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
|
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])
|
||||||
|
|
|
@ -60,19 +60,19 @@ emqx_authz_api_schema {
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
desc {
|
desc {
|
||||||
en: """List of HTTP headers."""
|
en: """List of HTTP Headers."""
|
||||||
zh: """"""
|
zh: """HTTP Headers 列表"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """headers"""
|
en: """Headers"""
|
||||||
zh: """请求头"""
|
zh: """请求头"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers_no_content_type {
|
headers_no_content_type {
|
||||||
desc {
|
desc {
|
||||||
en: """List of HTTP headers (without `content_type`)."""
|
en: """List of HTTP headers (without <code>content-type</code>)."""
|
||||||
zh: """"""
|
zh: """HTTP Headers 列表(无 <code>content-type</code>)"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """headers_no_content_type"""
|
en: """headers_no_content_type"""
|
||||||
|
|
|
@ -122,7 +122,7 @@ and the new rules will override all rules from the old config file.
|
||||||
http_get {
|
http_get {
|
||||||
desc {
|
desc {
|
||||||
en: """Authorization using an external HTTP server (via GET requests)."""
|
en: """Authorization using an external HTTP server (via GET requests)."""
|
||||||
zh: """使用外部 HTTP 服务器鉴权(GET 请求)."""
|
zh: """使用外部 HTTP 服务器鉴权(GET 请求)。"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """http_get"""
|
en: """http_get"""
|
||||||
|
@ -133,7 +133,7 @@ and the new rules will override all rules from the old config file.
|
||||||
http_post {
|
http_post {
|
||||||
desc {
|
desc {
|
||||||
en: """Authorization using an external HTTP server (via POST requests)."""
|
en: """Authorization using an external HTTP server (via POST requests)."""
|
||||||
zh: """使用外部 HTTP 服务器鉴权(POST 请求)."""
|
zh: """使用外部 HTTP 服务器鉴权(POST 请求)。"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """http_post"""
|
en: """http_post"""
|
||||||
|
@ -155,29 +155,29 @@ and the new rules will override all rules from the old config file.
|
||||||
url {
|
url {
|
||||||
desc {
|
desc {
|
||||||
en: """URL of the auth server."""
|
en: """URL of the auth server."""
|
||||||
zh: """认证服务器 URL"""
|
zh: """鉴权 HTTP 服务器地址。"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """url"""
|
en: """URL"""
|
||||||
zh: """url"""
|
zh: """URL"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
desc {
|
desc {
|
||||||
en: """List of HTTP headers."""
|
en: """List of HTTP Headers."""
|
||||||
zh: """"""
|
zh: """HTTP Headers 列表"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """headers"""
|
en: """Headers"""
|
||||||
zh: """请求头"""
|
zh: """请求头"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers_no_content_type {
|
headers_no_content_type {
|
||||||
desc {
|
desc {
|
||||||
en: """List of HTTP headers (without `content_type`)."""
|
en: """List of HTTP headers (without <code>content-type</code>)."""
|
||||||
zh: """"""
|
zh: """HTTP Headers 列表 (无 <code>content-type</code>) 。"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """headers_no_content_type"""
|
en: """headers_no_content_type"""
|
||||||
|
@ -188,22 +188,22 @@ and the new rules will override all rules from the old config file.
|
||||||
body {
|
body {
|
||||||
desc {
|
desc {
|
||||||
en: """HTTP request body."""
|
en: """HTTP request body."""
|
||||||
zh: """HTTP 请求体"""
|
zh: """HTTP request body。"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """body"""
|
en: """Request Body"""
|
||||||
zh: """请求体"""
|
zh: """Request Body"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request_timeout {
|
request_timeout {
|
||||||
desc {
|
desc {
|
||||||
en: """Request timeout."""
|
en: """HTTP request timeout."""
|
||||||
zh: """请求超时时间"""
|
zh: """HTTP 请求超时时长。"""
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
en: """request_timeout"""
|
en: """Request Timeout"""
|
||||||
zh: """请求超时"""
|
zh: """请求超时时间"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,9 +102,14 @@ authz_http_common_fields() ->
|
||||||
authz_common_fields(http) ++
|
authz_common_fields(http) ++
|
||||||
[
|
[
|
||||||
{url, fun url/1},
|
{url, fun url/1},
|
||||||
{body, map([{fuzzy, term(), binary()}])},
|
{body,
|
||||||
|
hoconsc:mk(map([{fuzzy, term(), binary()}]), #{
|
||||||
|
required => false, desc => ?DESC(body)
|
||||||
|
})},
|
||||||
{request_timeout,
|
{request_timeout,
|
||||||
mk_duration("Request timeout", #{default => "30s", desc => ?DESC(request_timeout)})}
|
mk_duration("Request timeout", #{
|
||||||
|
required => false, default => "30s", desc => ?DESC(request_timeout)
|
||||||
|
})}
|
||||||
] ++
|
] ++
|
||||||
maps:to_list(
|
maps:to_list(
|
||||||
maps:without(
|
maps:without(
|
||||||
|
@ -141,7 +146,10 @@ headers_no_content_type(desc) ->
|
||||||
?DESC(?FUNCTION_NAME);
|
?DESC(?FUNCTION_NAME);
|
||||||
headers_no_content_type(converter) ->
|
headers_no_content_type(converter) ->
|
||||||
fun(Headers) ->
|
fun(Headers) ->
|
||||||
|
maps:without(
|
||||||
|
[<<"content-type">>],
|
||||||
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
||||||
|
)
|
||||||
end;
|
end;
|
||||||
headers_no_content_type(default) ->
|
headers_no_content_type(default) ->
|
||||||
default_headers_no_content_type();
|
default_headers_no_content_type();
|
||||||
|
|
|
@ -94,45 +94,49 @@ authorize(
|
||||||
|
|
||||||
parse_config(
|
parse_config(
|
||||||
#{
|
#{
|
||||||
url := URL,
|
url := RawUrl,
|
||||||
method := Method,
|
method := Method,
|
||||||
headers := Headers,
|
headers := Headers,
|
||||||
request_timeout := ReqTimeout
|
request_timeout := ReqTimeout
|
||||||
} = Conf
|
} = Conf
|
||||||
) ->
|
) ->
|
||||||
{BaseURLWithPath, Query} = parse_fullpath(URL),
|
{BaseUrl0, Path, Query} = parse_url(RawUrl),
|
||||||
BaseURLMap = parse_url(BaseURLWithPath),
|
{ok, BaseUrl} = emqx_http_lib:uri_parse(BaseUrl0),
|
||||||
Conf#{
|
Conf#{
|
||||||
method => Method,
|
method => Method,
|
||||||
base_url => maps:remove(query, BaseURLMap),
|
base_url => BaseUrl,
|
||||||
|
headers => Headers,
|
||||||
|
base_path_templete => emqx_authz_utils:parse_str(Path, ?PLACEHOLDERS),
|
||||||
base_query_template => emqx_authz_utils:parse_deep(
|
base_query_template => emqx_authz_utils:parse_deep(
|
||||||
cow_qs:parse_qs(bin(Query)),
|
cow_qs:parse_qs(to_bin(Query)),
|
||||||
?PLACEHOLDERS
|
?PLACEHOLDERS
|
||||||
),
|
),
|
||||||
body_template => emqx_authz_utils:parse_deep(
|
body_template => emqx_authz_utils:parse_deep(
|
||||||
maps:to_list(maps:get(body, Conf, #{})),
|
maps:to_list(maps:get(body, Conf, #{})),
|
||||||
?PLACEHOLDERS
|
?PLACEHOLDERS
|
||||||
),
|
),
|
||||||
headers => Headers,
|
|
||||||
request_timeout => ReqTimeout,
|
request_timeout => ReqTimeout,
|
||||||
%% pool_type default value `random`
|
%% pool_type default value `random`
|
||||||
pool_type => random
|
pool_type => random
|
||||||
}.
|
}.
|
||||||
|
|
||||||
parse_fullpath(RawURL) ->
|
parse_url(Url) ->
|
||||||
cow_http:parse_fullpath(bin(RawURL)).
|
case string:split(Url, "//", leading) of
|
||||||
|
[Scheme, UrlRem] ->
|
||||||
parse_url(URL) when
|
case string:split(UrlRem, "/", leading) of
|
||||||
URL =:= undefined
|
[HostPort, Remaining] ->
|
||||||
->
|
BaseUrl = iolist_to_binary([Scheme, "//", HostPort]),
|
||||||
#{};
|
case string:split(Remaining, "?", leading) of
|
||||||
parse_url(URL) ->
|
[Path, QueryString] ->
|
||||||
{ok, URIMap} = emqx_http_lib:uri_parse(URL),
|
{BaseUrl, Path, QueryString};
|
||||||
case maps:get(query, URIMap, undefined) of
|
[Path] ->
|
||||||
undefined ->
|
{BaseUrl, Path, <<>>}
|
||||||
URIMap#{query => ""};
|
end;
|
||||||
_ ->
|
[HostPort] ->
|
||||||
URIMap
|
{iolist_to_binary([Scheme, "//", HostPort]), <<>>, <<>>}
|
||||||
|
end;
|
||||||
|
[Url] ->
|
||||||
|
throw({invalid_url, Url})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
generate_request(
|
generate_request(
|
||||||
|
@ -141,32 +145,33 @@ generate_request(
|
||||||
Client,
|
Client,
|
||||||
#{
|
#{
|
||||||
method := Method,
|
method := Method,
|
||||||
base_url := #{path := Path},
|
|
||||||
base_query_template := BaseQueryTemplate,
|
|
||||||
headers := Headers,
|
headers := Headers,
|
||||||
|
base_path_templete := BasePathTemplate,
|
||||||
|
base_query_template := BaseQueryTemplate,
|
||||||
body_template := BodyTemplate
|
body_template := BodyTemplate
|
||||||
}
|
}
|
||||||
) ->
|
) ->
|
||||||
Values = client_vars(Client, PubSub, Topic),
|
Values = client_vars(Client, PubSub, Topic),
|
||||||
|
Path = emqx_authz_utils:render_str(BasePathTemplate, Values),
|
||||||
|
Query = emqx_authz_utils:render_deep(BaseQueryTemplate, Values),
|
||||||
Body = emqx_authz_utils:render_deep(BodyTemplate, Values),
|
Body = emqx_authz_utils:render_deep(BodyTemplate, Values),
|
||||||
NBaseQuery = emqx_authz_utils:render_deep(BaseQueryTemplate, Values),
|
|
||||||
case Method of
|
case Method of
|
||||||
get ->
|
get ->
|
||||||
NPath = append_query(Path, NBaseQuery ++ Body),
|
NPath = append_query(Path, Query ++ Body),
|
||||||
{NPath, Headers};
|
{NPath, Headers};
|
||||||
_ ->
|
_ ->
|
||||||
NPath = append_query(Path, NBaseQuery),
|
NPath = append_query(Path, Query),
|
||||||
NBody = serialize_body(
|
NBody = serialize_body(
|
||||||
proplists:get_value(<<"Accept">>, Headers, <<"application/json">>),
|
proplists:get_value(<<"accept">>, Headers, <<"application/json">>),
|
||||||
Body
|
Body
|
||||||
),
|
),
|
||||||
{NPath, Headers, NBody}
|
{NPath, Headers, NBody}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
append_query(Path, []) ->
|
append_query(Path, []) ->
|
||||||
Path;
|
encode_path(Path);
|
||||||
append_query(Path, Query) ->
|
append_query(Path, Query) ->
|
||||||
Path ++ "?" ++ binary_to_list(query_string(Query)).
|
encode_path(Path) ++ "?" ++ to_list(query_string(Query)).
|
||||||
|
|
||||||
query_string(Body) ->
|
query_string(Body) ->
|
||||||
query_string(Body, []).
|
query_string(Body, []).
|
||||||
|
@ -179,13 +184,14 @@ query_string([], Acc) ->
|
||||||
<<>>
|
<<>>
|
||||||
end;
|
end;
|
||||||
query_string([{K, V} | More], Acc) ->
|
query_string([{K, V} | More], Acc) ->
|
||||||
query_string(
|
query_string(More, [["&", uri_encode(K), "=", uri_encode(V)] | Acc]).
|
||||||
More,
|
|
||||||
[
|
uri_encode(T) ->
|
||||||
["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)]
|
emqx_http_lib:uri_encode(to_list(T)).
|
||||||
| Acc
|
|
||||||
]
|
encode_path(Path) ->
|
||||||
).
|
Parts = string:split(Path, "/", all),
|
||||||
|
lists:flatten(["/" ++ Part || Part <- lists:map(fun uri_encode/1, Parts)]).
|
||||||
|
|
||||||
serialize_body(<<"application/json">>, Body) ->
|
serialize_body(<<"application/json">>, Body) ->
|
||||||
jsx:encode(Body);
|
jsx:encode(Body);
|
||||||
|
@ -198,7 +204,13 @@ client_vars(Client, PubSub, Topic) ->
|
||||||
topic => Topic
|
topic => Topic
|
||||||
}.
|
}.
|
||||||
|
|
||||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
to_list(A) when is_atom(A) ->
|
||||||
bin(B) when is_binary(B) -> B;
|
atom_to_list(A);
|
||||||
bin(L) when is_list(L) -> list_to_binary(L);
|
to_list(B) when is_binary(B) ->
|
||||||
bin(X) -> X.
|
binary_to_list(B);
|
||||||
|
to_list(L) when is_list(L) ->
|
||||||
|
L.
|
||||||
|
|
||||||
|
to_bin(B) when is_binary(B) -> B;
|
||||||
|
to_bin(L) when is_list(L) -> list_to_binary(L);
|
||||||
|
to_bin(X) -> X.
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
-type action() :: publish | subscribe | all.
|
-type action() :: publish | subscribe | all.
|
||||||
-type permission() :: allow | deny.
|
-type permission() :: allow | deny.
|
||||||
|
|
||||||
|
-import(emqx_schema, [mk_duration/2]).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
namespace/0,
|
namespace/0,
|
||||||
roots/0,
|
roots/0,
|
||||||
|
@ -249,8 +251,8 @@ http_common_fields() ->
|
||||||
[
|
[
|
||||||
{url, fun url/1},
|
{url, fun url/1},
|
||||||
{request_timeout,
|
{request_timeout,
|
||||||
emqx_schema:mk_duration("Request timeout", #{
|
mk_duration("Request timeout", #{
|
||||||
default => "30s", desc => ?DESC(request_timeout)
|
required => false, default => "30s", desc => ?DESC(request_timeout)
|
||||||
})},
|
})},
|
||||||
{body, #{type => map(), required => false, desc => ?DESC(body)}}
|
{body, #{type => map(), required => false, desc => ?DESC(body)}}
|
||||||
] ++
|
] ++
|
||||||
|
@ -303,7 +305,12 @@ headers_no_content_type(desc) ->
|
||||||
?DESC(?FUNCTION_NAME);
|
?DESC(?FUNCTION_NAME);
|
||||||
headers_no_content_type(converter) ->
|
headers_no_content_type(converter) ->
|
||||||
fun(Headers) ->
|
fun(Headers) ->
|
||||||
maps:to_list(maps:merge(default_headers_no_content_type(), transform_header_name(Headers)))
|
maps:to_list(
|
||||||
|
maps:without(
|
||||||
|
[<<"content-type">>],
|
||||||
|
maps:merge(default_headers_no_content_type(), transform_header_name(Headers))
|
||||||
|
)
|
||||||
|
)
|
||||||
end;
|
end;
|
||||||
headers_no_content_type(default) ->
|
headers_no_content_type(default) ->
|
||||||
default_headers_no_content_type();
|
default_headers_no_content_type();
|
||||||
|
@ -359,12 +366,12 @@ check_ssl_opts(Conf) ->
|
||||||
true;
|
true;
|
||||||
Url ->
|
Url ->
|
||||||
case emqx_authz_http:parse_url(Url) of
|
case emqx_authz_http:parse_url(Url) of
|
||||||
#{scheme := https} ->
|
{<<"https", _>>, _, _} ->
|
||||||
case hocon_maps:get("config.ssl.enable", Conf) of
|
case hocon_maps:get("config.ssl.enable", Conf) of
|
||||||
true -> true;
|
true -> true;
|
||||||
_ -> {error, ssl_not_enable}
|
_ -> {error, ssl_not_enable}
|
||||||
end;
|
end;
|
||||||
#{scheme := http} ->
|
{<<"http", _>>, _, _} ->
|
||||||
true;
|
true;
|
||||||
Bad ->
|
Bad ->
|
||||||
{bad_scheme, Url, Bad}
|
{bad_scheme, Url, Bad}
|
||||||
|
|
|
@ -25,8 +25,10 @@
|
||||||
create_resource/2,
|
create_resource/2,
|
||||||
update_config/2,
|
update_config/2,
|
||||||
parse_deep/2,
|
parse_deep/2,
|
||||||
|
parse_str/2,
|
||||||
parse_sql/3,
|
parse_sql/3,
|
||||||
render_deep/2,
|
render_deep/2,
|
||||||
|
render_str/2,
|
||||||
render_sql_params/2
|
render_sql_params/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -69,6 +71,9 @@ update_config(Path, ConfigRequest) ->
|
||||||
parse_deep(Template, PlaceHolders) ->
|
parse_deep(Template, PlaceHolders) ->
|
||||||
emqx_placeholder:preproc_tmpl_deep(Template, #{placeholders => PlaceHolders}).
|
emqx_placeholder:preproc_tmpl_deep(Template, #{placeholders => PlaceHolders}).
|
||||||
|
|
||||||
|
parse_str(Template, PlaceHolders) ->
|
||||||
|
emqx_placeholder:preproc_tmpl(Template, #{placeholders => PlaceHolders}).
|
||||||
|
|
||||||
parse_sql(Template, ReplaceWith, PlaceHolders) ->
|
parse_sql(Template, ReplaceWith, PlaceHolders) ->
|
||||||
emqx_placeholder:preproc_sql(
|
emqx_placeholder:preproc_sql(
|
||||||
Template,
|
Template,
|
||||||
|
@ -85,6 +90,13 @@ render_deep(Template, Values) ->
|
||||||
#{return => full_binary, var_trans => fun handle_var/2}
|
#{return => full_binary, var_trans => fun handle_var/2}
|
||||||
).
|
).
|
||||||
|
|
||||||
|
render_str(Template, Values) ->
|
||||||
|
emqx_placeholder:proc_tmpl(
|
||||||
|
Template,
|
||||||
|
client_vars(Values),
|
||||||
|
#{return => full_binary, var_trans => fun handle_var/2}
|
||||||
|
).
|
||||||
|
|
||||||
render_sql_params(ParamList, Values) ->
|
render_sql_params(ParamList, Values) ->
|
||||||
emqx_placeholder:proc_tmpl(
|
emqx_placeholder:proc_tmpl(
|
||||||
ParamList,
|
ParamList,
|
||||||
|
|
|
@ -201,6 +201,54 @@ t_query_params(_Config) ->
|
||||||
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
emqx_access_control:authorize(ClientInfo, publish, <<"t">>)
|
||||||
).
|
).
|
||||||
|
|
||||||
|
t_path(_Config) ->
|
||||||
|
ok = setup_handler_and_config(
|
||||||
|
fun(Req0, State) ->
|
||||||
|
?assertEqual(
|
||||||
|
<<
|
||||||
|
"/authz/users/"
|
||||||
|
"user%20name/"
|
||||||
|
"client%20id/"
|
||||||
|
"127.0.0.1/"
|
||||||
|
"MQTT/"
|
||||||
|
"MOUNTPOINT/"
|
||||||
|
"t/1/"
|
||||||
|
"publish"
|
||||||
|
>>,
|
||||||
|
cowboy_req:path(Req0)
|
||||||
|
),
|
||||||
|
Req = cowboy_req:reply(200, Req0),
|
||||||
|
{ok, Req, State}
|
||||||
|
end,
|
||||||
|
#{
|
||||||
|
<<"url">> => <<
|
||||||
|
"http://127.0.0.1:33333/authz/users/"
|
||||||
|
"${username}/"
|
||||||
|
"${clientid}/"
|
||||||
|
"${peerhost}/"
|
||||||
|
"${proto_name}/"
|
||||||
|
"${mountpoint}/"
|
||||||
|
"${topic}/"
|
||||||
|
"${action}"
|
||||||
|
>>
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
ClientInfo = #{
|
||||||
|
clientid => <<"client id">>,
|
||||||
|
username => <<"user name">>,
|
||||||
|
peerhost => {127, 0, 0, 1},
|
||||||
|
protocol => <<"MQTT">>,
|
||||||
|
mountpoint => <<"MOUNTPOINT">>,
|
||||||
|
zone => default,
|
||||||
|
listener => {tcp, default}
|
||||||
|
},
|
||||||
|
|
||||||
|
?assertEqual(
|
||||||
|
allow,
|
||||||
|
emqx_access_control:authorize(ClientInfo, publish, <<"t/1">>)
|
||||||
|
).
|
||||||
|
|
||||||
t_json_body(_Config) ->
|
t_json_body(_Config) ->
|
||||||
ok = setup_handler_and_config(
|
ok = setup_handler_and_config(
|
||||||
fun(Req0, State) ->
|
fun(Req0, State) ->
|
||||||
|
|
Loading…
Reference in New Issue