diff options
Diffstat (limited to 'deps/rabbit/src/rabbit_access_control.erl')
-rw-r--r-- | deps/rabbit/src/rabbit_access_control.erl | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/deps/rabbit/src/rabbit_access_control.erl b/deps/rabbit/src/rabbit_access_control.erl new file mode 100644 index 0000000000..72260d5723 --- /dev/null +++ b/deps/rabbit/src/rabbit_access_control.erl @@ -0,0 +1,257 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_access_control). + +-include("rabbit.hrl"). + +-export([check_user_pass_login/2, check_user_login/2, check_user_loopback/2, + check_vhost_access/4, check_resource_access/4, check_topic_access/4]). + +-export([permission_cache_can_expire/1, update_state/2]). + +%%---------------------------------------------------------------------------- + +-export_type([permission_atom/0]). + +-type permission_atom() :: 'configure' | 'read' | 'write'. + +%%---------------------------------------------------------------------------- + +-spec check_user_pass_login + (rabbit_types:username(), rabbit_types:password()) -> + {'ok', rabbit_types:user()} | + {'refused', rabbit_types:username(), string(), [any()]}. + +check_user_pass_login(Username, Password) -> + check_user_login(Username, [{password, Password}]). + +-spec check_user_login + (rabbit_types:username(), [{atom(), any()}]) -> + {'ok', rabbit_types:user()} | + {'refused', rabbit_types:username(), string(), [any()]}. + +check_user_login(Username, AuthProps) -> + %% extra auth properties like MQTT client id are in AuthProps + {ok, Modules} = application:get_env(rabbit, auth_backends), + R = lists:foldl( + fun (rabbit_auth_backend_cache=ModN, {refused, _, _, _}) -> + %% It is possible to specify authn/authz within the cache module settings, + %% so we have to do both auth steps here + %% See this rabbitmq-users discussion: + %% https://groups.google.com/d/topic/rabbitmq-users/ObqM7MQdA3I/discussion + try_authenticate_and_try_authorize(ModN, ModN, Username, AuthProps); + ({ModN, ModZs}, {refused, _, _, _}) -> + %% Different modules for authN vs authZ. So authenticate + %% with authN module, then if that succeeds do + %% passwordless (i.e pre-authenticated) login with authZ. + try_authenticate_and_try_authorize(ModN, ModZs, Username, AuthProps); + (Mod, {refused, _, _, _}) -> + %% Same module for authN and authZ. Just take the result + %% it gives us + case try_authenticate(Mod, Username, AuthProps) of + {ok, ModNUser = #auth_user{username = Username2, impl = Impl}} -> + rabbit_log:debug("User '~s' authenticated successfully by backend ~s", [Username2, Mod]), + user(ModNUser, {ok, [{Mod, Impl}], []}); + Else -> + rabbit_log:debug("User '~s' failed authenticatation by backend ~s", [Username, Mod]), + Else + end; + (_, {ok, User}) -> + %% We've successfully authenticated. Skip to the end... + {ok, User} + end, + {refused, Username, "No modules checked '~s'", [Username]}, Modules), + R. + +try_authenticate_and_try_authorize(ModN, ModZs0, Username, AuthProps) -> + ModZs = case ModZs0 of + A when is_atom(A) -> [A]; + L when is_list(L) -> L + end, + case try_authenticate(ModN, Username, AuthProps) of + {ok, ModNUser = #auth_user{username = Username2}} -> + rabbit_log:debug("User '~s' authenticated successfully by backend ~s", [Username2, ModN]), + user(ModNUser, try_authorize(ModZs, Username2, AuthProps)); + Else -> + Else + end. + +try_authenticate(Module, Username, AuthProps) -> + case Module:user_login_authentication(Username, AuthProps) of + {ok, AuthUser} -> {ok, AuthUser}; + {error, E} -> {refused, Username, + "~s failed authenticating ~s: ~p~n", + [Module, Username, E]}; + {refused, F, A} -> {refused, Username, F, A} + end. + +try_authorize(Modules, Username, AuthProps) -> + lists:foldr( + fun (Module, {ok, ModsImpls, ModsTags}) -> + case Module:user_login_authorization(Username, AuthProps) of + {ok, Impl, Tags}-> {ok, [{Module, Impl} | ModsImpls], ModsTags ++ Tags}; + {ok, Impl} -> {ok, [{Module, Impl} | ModsImpls], ModsTags}; + {error, E} -> {refused, Username, + "~s failed authorizing ~s: ~p~n", + [Module, Username, E]}; + {refused, F, A} -> {refused, Username, F, A} + end; + (_, {refused, F, A}) -> + {refused, Username, F, A} + end, {ok, [], []}, Modules). + +user(#auth_user{username = Username, tags = Tags}, {ok, ModZImpls, ModZTags}) -> + {ok, #user{username = Username, + tags = Tags ++ ModZTags, + authz_backends = ModZImpls}}; +user(_AuthUser, Error) -> + Error. + +auth_user(#user{username = Username, tags = Tags}, Impl) -> + #auth_user{username = Username, + tags = Tags, + impl = Impl}. + +-spec check_user_loopback + (rabbit_types:username(), rabbit_net:socket() | inet:ip_address()) -> + 'ok' | 'not_allowed'. + +check_user_loopback(Username, SockOrAddr) -> + {ok, Users} = application:get_env(rabbit, loopback_users), + case rabbit_net:is_loopback(SockOrAddr) + orelse not lists:member(Username, Users) of + true -> ok; + false -> not_allowed + end. + +get_authz_data_from({ip, Address}) -> + #{peeraddr => Address}; +get_authz_data_from({socket, Sock}) -> + {ok, {Address, _Port}} = rabbit_net:peername(Sock), + #{peeraddr => Address}; +get_authz_data_from(undefined) -> + undefined. + +% Note: ip can be either a tuple or, a binary if reverse_dns_lookups +% is enabled and it's a direct connection. +-spec check_vhost_access(User :: rabbit_types:user(), + VHostPath :: rabbit_types:vhost(), + AuthzRawData :: {socket, rabbit_net:socket()} | {ip, inet:ip_address() | binary()} | undefined, + AuthzContext :: map()) -> + 'ok' | rabbit_types:channel_exit(). +check_vhost_access(User = #user{username = Username, + authz_backends = Modules}, VHostPath, AuthzRawData, AuthzContext) -> + AuthzData = get_authz_data_from(AuthzRawData), + FullAuthzContext = create_vhost_access_authz_data(AuthzData, AuthzContext), + lists:foldl( + fun({Mod, Impl}, ok) -> + check_access( + fun() -> + rabbit_vhost:exists(VHostPath) andalso + Mod:check_vhost_access( + auth_user(User, Impl), VHostPath, FullAuthzContext) + end, + Mod, "access to vhost '~s' refused for user '~s'", + [VHostPath, Username], not_allowed); + (_, Else) -> + Else + end, ok, Modules). + +create_vhost_access_authz_data(undefined, Context) when map_size(Context) == 0 -> + undefined; +create_vhost_access_authz_data(undefined, Context) -> + Context; +create_vhost_access_authz_data(PeerAddr, Context) when map_size(Context) == 0 -> + PeerAddr; +create_vhost_access_authz_data(PeerAddr, Context) -> + maps:merge(PeerAddr, Context). + +-spec check_resource_access + (rabbit_types:user(), rabbit_types:r(atom()), permission_atom(), rabbit_types:authz_context()) -> + 'ok' | rabbit_types:channel_exit(). + +check_resource_access(User, R = #resource{kind = exchange, name = <<"">>}, + Permission, Context) -> + check_resource_access(User, R#resource{name = <<"amq.default">>}, + Permission, Context); +check_resource_access(User = #user{username = Username, + authz_backends = Modules}, + Resource, Permission, Context) -> + lists:foldl( + fun({Module, Impl}, ok) -> + check_access( + fun() -> Module:check_resource_access( + auth_user(User, Impl), Resource, Permission, Context) end, + Module, "access to ~s refused for user '~s'", + [rabbit_misc:rs(Resource), Username]); + (_, Else) -> Else + end, ok, Modules). + +check_topic_access(User = #user{username = Username, + authz_backends = Modules}, + Resource, Permission, Context) -> + lists:foldl( + fun({Module, Impl}, ok) -> + check_access( + fun() -> Module:check_topic_access( + auth_user(User, Impl), Resource, Permission, Context) end, + Module, "access to topic '~s' in exchange ~s refused for user '~s'", + [maps:get(routing_key, Context), rabbit_misc:rs(Resource), Username]); + (_, Else) -> Else + end, ok, Modules). + +check_access(Fun, Module, ErrStr, ErrArgs) -> + check_access(Fun, Module, ErrStr, ErrArgs, access_refused). + +check_access(Fun, Module, ErrStr, ErrArgs, ErrName) -> + case Fun() of + true -> + ok; + false -> + rabbit_misc:protocol_error(ErrName, ErrStr, ErrArgs); + {error, E} -> + FullErrStr = ErrStr ++ ", backend ~s returned an error: ~p~n", + FullErrArgs = ErrArgs ++ [Module, E], + rabbit_log:error(FullErrStr, FullErrArgs), + rabbit_misc:protocol_error(ErrName, FullErrStr, FullErrArgs) + end. + +-spec update_state(User :: rabbit_types:user(), NewState :: term()) -> + {'ok', rabbit_types:auth_user()} | + {'refused', string()} | + {'error', any()}. + +update_state(User = #user{authz_backends = Backends0}, NewState) -> + %% N.B.: we use foldl/3 and prepending, so the final list of + %% backends is in reverse order from the original list. + Backends = lists:foldl( + fun({Module, Impl}, {ok, Acc}) -> + case Module:state_can_expire() of + true -> + case Module:update_state(auth_user(User, Impl), NewState) of + {ok, #auth_user{impl = Impl1}} -> + {ok, [{Module, Impl1} | Acc]}; + Else -> Else + end; + false -> + {ok, [{Module, Impl} | Acc]} + end; + (_, {error, _} = Err) -> Err; + (_, {refused, _, _} = Err) -> Err + end, {ok, []}, Backends0), + case Backends of + {ok, Pairs} -> {ok, User#user{authz_backends = lists:reverse(Pairs)}}; + Else -> Else + end. + +-spec permission_cache_can_expire(User :: rabbit_types:user()) -> boolean(). + +%% Returns true if any of the backends support credential expiration, +%% otherwise returns false. +permission_cache_can_expire(#user{authz_backends = Backends}) -> + lists:any(fun ({Module, _State}) -> Module:state_can_expire() end, Backends). |