Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deps/rabbit/src/rabbit_access_control.erl
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,10 @@ check_access(Fun, Module, ErrStr, ErrArgs, ErrName) ->
ok;
false ->
rabbit_misc:protocol_error(ErrName, ErrStr, ErrArgs);
{false, Reason} ->
FullErrStr = ErrStr ++ " by backend ~ts: ~ts",
FullErrArgs = ErrArgs ++ [Module, Reason],
rabbit_misc:protocol_error(ErrName, FullErrStr, FullErrArgs);
{error, E} ->
FullErrStr = ErrStr ++ ", backend ~ts returned an error: ~tp",
FullErrArgs = ErrArgs ++ [Module, E],
Expand Down
9 changes: 6 additions & 3 deletions deps/rabbit/src/rabbit_authz_backend.erl
Original file line number Diff line number Diff line change
Expand Up @@ -32,38 +32,41 @@
%% Possible responses:
%% true
%% false
%% {false, Reason}
%% {error, Error}
%% Something went wrong. Log and die.
-callback check_vhost_access(AuthUser :: rabbit_types:auth_user(),
VHost :: rabbit_types:vhost(),
AuthzData :: rabbit_types:authz_data()) ->
boolean() | {'error', any()}.
boolean() | {false, Reason :: string()} | {'error', any()}.

%% Given #auth_user, resource and permission, can a user access a resource?
%%
%% Possible responses:
%% true
%% false
%% {false, Reason}
%% {error, Error}
%% Something went wrong. Log and die.
-callback check_resource_access(rabbit_types:auth_user(),
rabbit_types:r(atom()),
rabbit_types:permission_atom(),
rabbit_types:authz_context()) ->
boolean() | {'error', any()}.
boolean() | {false, Reason :: string()} | {'error', any()}.

%% Given #auth_user, topic as resource, permission, and context, can a user access the topic?
%%
%% Possible responses:
%% true
%% false
%% {false, Reason}
%% {error, Error}
%% Something went wrong. Log and die.
-callback check_topic_access(rabbit_types:auth_user(),
rabbit_types:r(atom()),
rabbit_types:permission_atom(),
rabbit_types:topic_access_context()) ->
boolean() | {'error', any()}.
boolean() | {false, Reason :: string()} | {'error', any()}.

%% Updates backend state that has expired.
%%
Expand Down
33 changes: 15 additions & 18 deletions deps/rabbitmq_auth_backend_cache/src/rabbit_auth_backend_cache.erl
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,34 @@ user_login_authorization(Username, AuthProps) ->
end).

check_vhost_access(#auth_user{} = AuthUser, VHostPath, AuthzData) ->
with_cache(authz, {check_vhost_access, [AuthUser, VHostPath, AuthzData]},
fun(true) -> success;
(false) -> refusal;
({error, _} = Err) -> Err;
(_) -> unknown
end).
with_cache(authz,
{check_vhost_access, [AuthUser, VHostPath, AuthzData]},
fun convert_backend_result/1).

check_resource_access(#auth_user{} = AuthUser,
#resource{} = Resource, Permission, AuthzContext) ->
with_cache(authz, {check_resource_access, [AuthUser, Resource, Permission, AuthzContext]},
fun(true) -> success;
(false) -> refusal;
({error, _} = Err) -> Err;
(_) -> unknown
end).
with_cache(authz,
{check_resource_access, [AuthUser, Resource, Permission, AuthzContext]},
fun convert_backend_result/1).

check_topic_access(#auth_user{} = AuthUser,
#resource{} = Resource, Permission, Context) ->
with_cache(authz, {check_topic_access, [AuthUser, Resource, Permission, Context]},
fun(true) -> success;
(false) -> refusal;
({error, _} = Err) -> Err;
(_) -> unknown
end).
with_cache(authz,
{check_topic_access, [AuthUser, Resource, Permission, Context]},
fun convert_backend_result/1).

expiry_timestamp(_) -> never.

%%
%% Implementation
%%

convert_backend_result(true) -> success;
convert_backend_result(false) -> refusal;
convert_backend_result({false, _}) -> refusal;
convert_backend_result({error, _} = Err) -> Err;
convert_backend_result(_) -> unknown.

clear_cache_cluster_wide() ->
Nodes = rabbit_nodes:list_running(),
?LOG_WARNING("Clearing auth_backend_cache in all nodes : ~p", [Nodes]),
Expand Down
4 changes: 2 additions & 2 deletions deps/rabbitmq_auth_backend_cache/src/rabbit_auth_cache.erl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
-callback clear() -> ok.

expiration(TTL) ->
erlang:system_time(milli_seconds) + TTL.
erlang:system_time(millisecond) + TTL.

expired(Exp) ->
erlang:system_time(milli_seconds) > Exp.
erlang:system_time(millisecond) > Exp.
5 changes: 3 additions & 2 deletions deps/rabbitmq_auth_backend_http/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ define PROJECT_ENV
{user_path, "http://localhost:8000/auth/user"},
{vhost_path, "http://localhost:8000/auth/vhost"},
{resource_path, "http://localhost:8000/auth/resource"},
{topic_path, "http://localhost:8000/auth/topic"}
{topic_path, "http://localhost:8000/auth/topic"},
{authorization_failure_disclosure, false}
]
endef

Expand All @@ -20,7 +21,7 @@ endef

LOCAL_DEPS = ssl inets crypto public_key
DEPS = rabbit_common rabbit amqp_client
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers cowboy
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers cowboy rabbitmq_amqp_client

DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk
DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk
Expand Down
8 changes: 6 additions & 2 deletions deps/rabbitmq_auth_backend_http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ auth_http.user_path = http://some-server/auth/user
auth_http.vhost_path = http://some-server/auth/vhost
auth_http.resource_path = http://some-server/auth/resource
auth_http.topic_path = http://some-server/auth/topic
auth_http.authorization_failure_disclosure = false
```

In the [`advanced.config` format](https://www.rabbitmq.com/configure.html#advanced-config-file):
Expand All @@ -65,7 +66,8 @@ In the [`advanced.config` format](https://www.rabbitmq.com/configure.html#advanc
{user_path, "http(s)://some-server/auth/user"},
{vhost_path, "http(s)://some-server/auth/vhost"},
{resource_path, "http(s)://some-server/auth/resource"},
{topic_path, "http(s)://some-server/auth/topic"}]}
{topic_path, "http(s)://some-server/auth/topic"},
{authorization_failure_disclosure, false}]}
].
```

Expand Down Expand Up @@ -123,11 +125,13 @@ Your web server should always return HTTP 200 OK, with a body
containing:

* `deny`: deny access to the user / vhost / resource
* `deny <Reason>`: deny access to the user / vhost / resource. RabbitMQ will log the `<Reason>` at INFO level.
If `auth_http.authorization_failure_disclosure` is set to `true` (the default is `false` for security reasons)
RabbitMQ will additionally forward the `<Reason>` to AMQP clients.
* `allow`: allow access to the user / vhost / resource
* `allow [list of tags]` (for `user_path` only): allow access, and mark the user as an having the tags listed

## Using TLS/HTTPS

If your Web server uses HTTPS and certificate verification, you need to
configure the plugin to use a CA and client certificate/key pair using the `rabbitmq_auth_backend_http.ssl_options` config variable:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
{mapping, "auth_http.connection_timeout", "rabbitmq_auth_backend_http.connection_timeout",
[{datatype, integer}]}.

{mapping, "auth_http.authorization_failure_disclosure", "rabbitmq_auth_backend_http.authorization_failure_disclosure", [
{datatype, {enum, [true, false]}}]}.

%% TLS options

{mapping, "auth_http.ssl_options", "rabbitmq_auth_backend_http.ssl_options", [
Expand Down
124 changes: 79 additions & 45 deletions deps/rabbitmq_auth_backend_http/src/rabbit_auth_backend_http.erl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

-define(SUCCESSFUL_RESPONSE_CODES, [200, 201]).

-define(APP, rabbitmq_auth_backend_http).

%%--------------------------------------------------------------------

description() ->
Expand All @@ -34,16 +36,29 @@ description() ->
%%--------------------------------------------------------------------

user_login_authentication(Username, AuthProps) ->
case http_req(p(user_path), q([{username, Username}] ++ extract_other_credentials(AuthProps))) of
{error, _} = E -> E;
"deny" -> {refused, "Denied by the backing HTTP service", []};
"allow" ++ Rest -> Tags = [rabbit_data_coercion:to_atom(T) ||
T <- string:tokens(Rest, " ")],

{ok, #auth_user{username = Username,
tags = Tags,
impl = fun() -> proplists:delete(username, AuthProps) end}};
Other -> {error, {bad_response, Other}}
Path = p(user_path),
Query = q([{username, Username}] ++ extract_other_credentials(AuthProps)),
case http_req(Path, Query) of
{error, _} = Err ->
Err;
"deny " ++ Reason ->
?LOG_INFO("HTTP authentication denied for user '~ts': ~ts",
[Username, Reason]),
{refused, "Denied by the backing HTTP service", []};
Body ->
case string:lowercase(Body) of
"deny" ->
{refused, "Denied by the backing HTTP service", []};
"allow" ++ Rest ->
Tags = [rabbit_data_coercion:to_atom(T)
|| T <- string:tokens(Rest, " ")],
{ok, #auth_user{
username = Username,
tags = Tags,
impl = fun() -> proplists:delete(username, AuthProps) end}};
Other ->
{error, {bad_response, Other}}
end
end.

%% When a protocol plugin uses an internal AMQP 0-9-1 client to interact with RabbitMQ core,
Expand Down Expand Up @@ -109,34 +124,34 @@ check_vhost_access(#auth_user{username = Username, tags = Tags}, VHost,

do_check_vhost_access(Username, Tags, VHost, Ip, AuthzData) ->
OptionsParameters = context_as_parameters(AuthzData),
bool_req(vhost_path, [{username, Username},
{vhost, VHost},
{ip, Ip},
{tags, join_tags(Tags)}] ++ OptionsParameters).
req(vhost_path, [{username, Username},
{vhost, VHost},
{ip, Ip},
{tags, join_tags(Tags)}] ++ OptionsParameters).

check_resource_access(#auth_user{username = Username, tags = Tags},
#resource{virtual_host = VHost, kind = Type, name = Name},
Permission,
AuthzContext) ->
OptionsParameters = context_as_parameters(AuthzContext),
bool_req(resource_path, [{username, Username},
{vhost, VHost},
{resource, Type},
{name, Name},
{permission, Permission},
{tags, join_tags(Tags)}] ++ OptionsParameters).
req(resource_path, [{username, Username},
{vhost, VHost},
{resource, Type},
{name, Name},
{permission, Permission},
{tags, join_tags(Tags)}] ++ OptionsParameters).

check_topic_access(#auth_user{username = Username, tags = Tags},
#resource{virtual_host = VHost, kind = topic = Type, name = Name},
Permission,
Context) ->
OptionsParameters = context_as_parameters(Context),
bool_req(topic_path, [{username, Username},
{vhost, VHost},
{resource, Type},
{name, Name},
{permission, Permission},
{tags, join_tags(Tags)}] ++ OptionsParameters).
req(topic_path, [{username, Username},
{vhost, VHost},
{resource, Type},
{name, Name},
{permission, Permission},
{tags, join_tags(Tags)}] ++ OptionsParameters).

expiry_timestamp(_) -> never.

Expand All @@ -152,14 +167,32 @@ context_as_parameters(Options) when is_map(Options) ->
context_as_parameters(_) ->
[].

bool_req(PathName, Props) ->
case http_req(p(PathName), q(Props)) of
"deny" -> false;
"allow" -> true;
E -> E
req(PathName, Props) ->
Path = p(PathName),
Query = q(Props),
case http_req(Path, Query) of
{error, _} = Err ->
Err;
"deny " ++ Reason ->
?LOG_INFO("HTTP authorization denied for path ~ts with query '~ts': ~ts",
[Path, Query, Reason]),
case application:get_env(?APP, authorization_failure_disclosure) of
{ok, true} ->
{false, Reason};
_ ->
false
end;
Body ->
case string:lowercase(Body) of
"deny" ->
false;
"allow" ->
true
end
end.

http_req(Path, Query) -> http_req(Path, Query, ?RETRY_ON_KEEPALIVE_CLOSED).
http_req(Path, Query) ->
http_req(Path, Query, ?RETRY_ON_KEEPALIVE_CLOSED).

http_req(Path, Query, Retry) ->
case do_http_req(Path, Query) of
Expand All @@ -177,7 +210,7 @@ do_http_req(Path0, Query) ->
{host, Host} = lists:keyfind(host, 1, URI),
{port, Port} = lists:keyfind(port, 1, URI),
HostHdr = rabbit_misc:format("~ts:~b", [Host, Port]),
{ok, Method} = application:get_env(rabbitmq_auth_backend_http, http_method),
{ok, Method} = application:get_env(?APP, http_method),
Request = case rabbit_data_coercion:to_atom(Method) of
get ->
Path = Path0 ++ "?" ++ Query,
Expand All @@ -188,34 +221,37 @@ do_http_req(Path0, Query) ->
{Path0, [{"Host", HostHdr}], "application/x-www-form-urlencoded", Query}
end,
RequestTimeout =
case application:get_env(rabbitmq_auth_backend_http, request_timeout) of
case application:get_env(?APP, request_timeout) of
{ok, Val1} -> Val1;
_ -> infinity
end,
ConnectionTimeout =
case application:get_env(rabbitmq_auth_backend_http, connection_timeout) of
case application:get_env(?APP, connection_timeout) of
{ok, Val2} -> Val2;
_ -> RequestTimeout
end,
?LOG_DEBUG("auth_backend_http: request timeout: ~tp, connection timeout: ~tp", [RequestTimeout, ConnectionTimeout]),
HttpOpts = [{timeout, RequestTimeout},
{connect_timeout, ConnectionTimeout}] ++ ssl_options(),
case httpc:request(Method, Request, HttpOpts, []) of
{ok, {{_HTTP, Code, _}, _Headers, Body}} ->
?LOG_DEBUG("auth_backend_http: response code is ~tp, body: ~tp", [Code, Body]),
case httpc:request(Method, Request, HttpOpts, [{body_format, binary}]) of
{ok, {{_HTTP, Code, _}, _Headers, BodyBin}} ->
Body = unicode:characters_to_list(BodyBin),
?LOG_DEBUG("auth_backend_http: response code is ~tp, body: '~ts'", [Code, Body]),
case lists:member(Code, ?SUCCESSFUL_RESPONSE_CODES) of
true -> parse_resp(Body);
false -> {error, {Code, Body}}
true ->
string:strip(Body);
false ->
{error, {Code, Body}}
end;
{error, _} = E ->
E
end.

ssl_options() ->
case application:get_env(rabbitmq_auth_backend_http, ssl_options) of
case application:get_env(?APP, ssl_options) of
{ok, Opts0} when is_list(Opts0) ->
Opts1 = [{ssl, rabbit_ssl_options:fix_client(Opts0)}],
case application:get_env(rabbitmq_auth_backend_http, ssl_hostname_verification) of
case application:get_env(?APP, ssl_hostname_verification) of
{ok, wildcard} ->
?LOG_DEBUG("Enabling wildcard-aware hostname verification for HTTP client connections"),
%% Needed for HTTPS connections that connect to servers that use wildcard certificates.
Expand All @@ -228,7 +264,7 @@ ssl_options() ->
end.

p(PathName) ->
{ok, Path} = application:get_env(rabbitmq_auth_backend_http, PathName),
{ok, Path} = application:get_env(?APP, PathName),
Path.

q(Args) ->
Expand All @@ -240,8 +276,6 @@ escape(K, Map) when is_map(Map) ->
escape(K, V) ->
rabbit_data_coercion:to_list(K) ++ "=" ++ rabbit_http_util:quote_plus(V).

parse_resp(Resp) -> string:to_lower(string:strip(Resp)).

join_tags([]) -> "";
join_tags(Tags) ->
Strings = [rabbit_data_coercion:to_list(T) || T <- Tags],
Expand Down
Loading
Loading