Merge branch 'master' into delegate_opt
This commit is contained in:
		
						commit
						8a0ad56182
					
				|  | @ -13,6 +13,7 @@ on: | |||
|       - '*.bzl' | ||||
|       - '*.bazel' | ||||
|       - .github/workflows/test.yaml | ||||
|   pull_request: | ||||
| jobs: | ||||
|   test: | ||||
|     name: Test | ||||
|  |  | |||
|  | @ -34,13 +34,13 @@ buildbuddy( | |||
| 
 | ||||
| git_repository( | ||||
|     name = "rbe_23", | ||||
|     commit = "d2b454dc5138a2a92de45a0a672241a4fbb5a1e5", | ||||
|     commit = "b21c066e426de48e526cc0f8c5158b7024d04e85", | ||||
|     remote = "https://github.com/rabbitmq/rbe-erlang-platform.git", | ||||
| ) | ||||
| 
 | ||||
| git_repository( | ||||
|     name = "rbe_24", | ||||
|     commit = "a087892ef4202dc3245b64d36d5921491848315f", | ||||
|     commit = "c8cbf65e2facbe464ebbcee7b6cf6f7a2d422ded", | ||||
|     remote = "https://github.com/rabbitmq/rbe-erlang-platform.git", | ||||
| ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -703,9 +703,6 @@ suites = [ | |||
|         additional_hdrs = [ | ||||
|             "src/rabbit_fifo.hrl", | ||||
|         ], | ||||
|         erlc_opts = [ | ||||
|             "-I deps/rabbit",  # allow rabbit_fifo.hrl to be included at src/rabbit_fifo.hrl | ||||
|         ], | ||||
|         runtime_deps = [ | ||||
|             "@meck//:bazel_erlang_lib", | ||||
|             "@ra//:bazel_erlang_lib", | ||||
|  |  | |||
|  | @ -500,7 +500,7 @@ stop_and_halt() -> | |||
|         %% init:stop() will be called regardless of any errors. | ||||
|         try | ||||
|             AppsLeft = [ A || {A, _, _} <- application:which_applications() ], | ||||
|             ?LOG_ERROR( | ||||
|             ?LOG_INFO( | ||||
|                 lists:flatten( | ||||
|                   ["Halting Erlang VM with the following applications:~n", | ||||
|                    ["    ~p~n" || _ <- AppsLeft]]), | ||||
|  |  | |||
|  | @ -38,35 +38,42 @@ check_user_pass_login(Username, Password) -> | |||
| 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 | ||||
|         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) | ||||
|         catch  | ||||
|             Type:Error:Stacktrace ->  | ||||
|                 rabbit_log:debug("User '~s' authentication failed with ~s:~p:~n~p", [Username, Type, Error, Stacktrace]), | ||||
|                 {refused, Username, "User '~s' authentication failed with internal error. " | ||||
|                                     "Enable debug logs to see the real error.", [Username]} | ||||
| 
 | ||||
|         end. | ||||
| 
 | ||||
| try_authenticate_and_try_authorize(ModN, ModZs0, Username, AuthProps) -> | ||||
|     ModZs = case ModZs0 of | ||||
|  |  | |||
|  | @ -14,12 +14,15 @@ | |||
| -export([user_login_authentication/2, user_login_authorization/2, | ||||
|          check_vhost_access/3, check_resource_access/4, check_topic_access/4]). | ||||
| 
 | ||||
| -export([add_user/3, delete_user/2, lookup_user/1, exists/1, | ||||
| -export([add_user/3, add_user/4, add_user/5, delete_user/2, lookup_user/1, exists/1, | ||||
|          change_password/3, clear_password/2, | ||||
|          hash_password/2, change_password_hash/2, change_password_hash/3, | ||||
|          set_tags/3, set_permissions/6, clear_permissions/3, | ||||
|          set_topic_permissions/6, clear_topic_permissions/3, clear_topic_permissions/4, | ||||
|          add_user_sans_validation/3, put_user/2, put_user/3]). | ||||
|          add_user_sans_validation/3, put_user/2, put_user/3, | ||||
|          update_user/5, | ||||
|          update_user_with_hash/5, | ||||
|          add_user_sans_validation/6]). | ||||
| 
 | ||||
| -export([set_user_limits/3, clear_user_limits/3, is_over_connection_limit/1, | ||||
|          is_over_channel_limit/1, get_user_limits/0, get_user_limits/1]). | ||||
|  | @ -208,14 +211,56 @@ add_user(Username, Password, ActingUser) -> | |||
|     validate_and_alternate_credentials(Username, Password, ActingUser, | ||||
|                                        fun add_user_sans_validation/3). | ||||
| 
 | ||||
| -spec add_user(rabbit_types:username(), rabbit_types:password(), | ||||
|                rabbit_types:username(), [atom()]) -> 'ok' | {'error', string()}. | ||||
| 
 | ||||
| add_user(Username, Password, ActingUser, Tags) -> | ||||
|     add_user(Username, Password, ActingUser, undefined, Tags). | ||||
| 
 | ||||
| add_user(Username, Password, ActingUser, Limits, Tags) -> | ||||
|     validate_and_alternate_credentials(Username, Password, ActingUser, | ||||
|                                        add_user_sans_validation(Limits, Tags)). | ||||
| 
 | ||||
| add_user_sans_validation(Username, Password, ActingUser) -> | ||||
|     add_user_sans_validation(Username, Password, ActingUser, undefined, []). | ||||
| 
 | ||||
| add_user_sans_validation(Limits, Tags) -> | ||||
|     fun(Username, Password, ActingUser) -> | ||||
|             add_user_sans_validation(Username, Password, ActingUser, Limits, Tags) | ||||
|     end. | ||||
| 
 | ||||
| add_user_sans_validation(Username, Password, ActingUser, Limits, Tags) -> | ||||
|     rabbit_log:debug("Asked to create a new user '~s', password length in bytes: ~p", [Username, bit_size(Password)]), | ||||
|     %% hash_password will pick the hashing function configured for us | ||||
|     %% but we also need to store a hint as part of the record, so we | ||||
|     %% retrieve it here one more time | ||||
|     HashingMod = rabbit_password:hashing_mod(), | ||||
|     PasswordHash = hash_password(HashingMod, Password), | ||||
|     User = internal_user:create_user(Username, PasswordHash, HashingMod), | ||||
|     User0 = internal_user:create_user(Username, PasswordHash, HashingMod), | ||||
|     ConvertedTags = [rabbit_data_coercion:to_atom(I) || I <- Tags], | ||||
|     User1 = internal_user:set_tags(User0, ConvertedTags), | ||||
|     User = case Limits of | ||||
|                undefined -> User1; | ||||
|                Term -> internal_user:update_limits(add, User1, Term) | ||||
|            end, | ||||
|     add_user_sans_validation_in(Username, User, ConvertedTags, Limits, ActingUser). | ||||
| 
 | ||||
| add_user_sans_validation(Username, PasswordHash, HashingAlgorithm, Tags, Limits, ActingUser) -> | ||||
|     rabbit_log:debug("Asked to create a new user '~s' with password hash", [Username]), | ||||
|     ConvertedTags = [rabbit_data_coercion:to_atom(I) || I <- Tags], | ||||
|     HashingMod = rabbit_password:hashing_mod(), | ||||
|     User0 = internal_user:create_user(Username, PasswordHash, HashingMod), | ||||
|     User1 = internal_user:set_tags( | ||||
|               internal_user:set_password_hash(User0, | ||||
|                                               PasswordHash, HashingAlgorithm), | ||||
|               ConvertedTags), | ||||
|     User = case Limits of | ||||
|                undefined -> User1; | ||||
|                Term -> internal_user:update_limits(add, User1, Term) | ||||
|            end, | ||||
|     add_user_sans_validation_in(Username, User, ConvertedTags, Limits, ActingUser). | ||||
| 
 | ||||
| add_user_sans_validation_in(Username, User, ConvertedTags, Limits, ActingUser) -> | ||||
|     try | ||||
|         R = rabbit_misc:execute_mnesia_transaction( | ||||
|           fun () -> | ||||
|  | @ -229,6 +274,14 @@ add_user_sans_validation(Username, Password, ActingUser) -> | |||
|         rabbit_log:info("Created user '~s'", [Username]), | ||||
|         rabbit_event:notify(user_created, [{name, Username}, | ||||
|                                            {user_who_performed_action, ActingUser}]), | ||||
|         case ConvertedTags of | ||||
|             [] -> ok; | ||||
|             _ -> notify_user_tags_set(Username, ConvertedTags, ActingUser) | ||||
|         end, | ||||
|         case Limits of | ||||
|             undefined -> ok; | ||||
|             _ -> notify_limit_set(Username, ActingUser, Limits) | ||||
|         end, | ||||
|         R | ||||
|     catch | ||||
|         throw:{error, {user_already_exists, _}} = Error -> | ||||
|  | @ -322,6 +375,42 @@ change_password_sans_validation(Username, Password, ActingUser) -> | |||
|             erlang:raise(Class, Error, Stacktrace) | ||||
|     end. | ||||
| 
 | ||||
| update_user(Username, Password, Tags, Limits, ActingUser) -> | ||||
|     validate_and_alternate_credentials(Username, Password, ActingUser, | ||||
|                                        update_user_sans_validation(Tags, Limits)). | ||||
| 
 | ||||
| update_user_sans_validation(Tags, Limits) -> | ||||
|     fun(Username, Password, ActingUser) -> | ||||
|             try | ||||
|                 rabbit_log:debug("Asked to change password of user '~s', new password length in bytes: ~p", [Username, bit_size(Password)]), | ||||
|                 HashingAlgorithm = rabbit_password:hashing_mod(), | ||||
| 
 | ||||
|                 rabbit_log:debug("Asked to set user tags for user '~s' to ~p", [Username, Tags]), | ||||
| 
 | ||||
|                 ConvertedTags = [rabbit_data_coercion:to_atom(I) || I <- Tags], | ||||
|                 R = update_user_with_hash(Username, | ||||
|                                           hash_password(rabbit_password:hashing_mod(), | ||||
|                                                         Password), | ||||
|                                           HashingAlgorithm, | ||||
|                                           ConvertedTags, | ||||
|                                           Limits), | ||||
|                 rabbit_log:info("Successfully changed password for user '~s'", [Username]), | ||||
|                 rabbit_event:notify(user_password_changed, | ||||
|                                     [{name, Username}, | ||||
|                                      {user_who_performed_action, ActingUser}]), | ||||
| 
 | ||||
|                 notify_user_tags_set(Username, ConvertedTags, ActingUser), | ||||
|                 R | ||||
|             catch | ||||
|                 throw:{error, {no_such_user, _}} = Error -> | ||||
|                     rabbit_log:warning("Failed to change password for user '~s': the user does not exist", [Username]), | ||||
|                     throw(Error); | ||||
|                 Class:Error:Stacktrace -> | ||||
|                     rabbit_log:warning("Failed to change password for user '~s': ~p", [Username, Error]), | ||||
|                     erlang:raise(Class, Error, Stacktrace) | ||||
|             end | ||||
|     end. | ||||
| 
 | ||||
| -spec clear_password(rabbit_types:username(), rabbit_types:username()) -> 'ok'. | ||||
| 
 | ||||
| clear_password(Username, ActingUser) -> | ||||
|  | @ -346,10 +435,22 @@ change_password_hash(Username, PasswordHash) -> | |||
| 
 | ||||
| 
 | ||||
| change_password_hash(Username, PasswordHash, HashingAlgorithm) -> | ||||
|     update_user(Username, fun(User) -> | ||||
|                               internal_user:set_password_hash(User, | ||||
|                                   PasswordHash, HashingAlgorithm) | ||||
|                           end). | ||||
|     update_user_with_hash(Username, PasswordHash, HashingAlgorithm, [], undefined). | ||||
| 
 | ||||
| update_user_with_hash(Username, PasswordHash, HashingAlgorithm, ConvertedTags, Limits) -> | ||||
|     update_user(Username, | ||||
|                 fun(User0) -> | ||||
|                         User1 = internal_user:set_password_hash(User0, | ||||
|                                                                 PasswordHash, HashingAlgorithm), | ||||
|                         User2 = case Limits of | ||||
|                                     undefined -> User1; | ||||
|                                     _         -> internal_user:update_limits(add, User1, Limits) | ||||
|                                 end, | ||||
|                         case ConvertedTags of | ||||
|                             [] -> User2; | ||||
|                             _  -> internal_user:set_tags(User2, ConvertedTags) | ||||
|                         end | ||||
|                 end). | ||||
| 
 | ||||
| -spec set_tags(rabbit_types:username(), [atom()], rabbit_types:username()) -> 'ok'. | ||||
| 
 | ||||
|  | @ -360,9 +461,7 @@ set_tags(Username, Tags, ActingUser) -> | |||
|         R = update_user(Username, fun(User) -> | ||||
|                                      internal_user:set_tags(User, ConvertedTags) | ||||
|                                   end), | ||||
|         rabbit_log:info("Successfully set user tags for user '~s' to ~p", [Username, ConvertedTags]), | ||||
|         rabbit_event:notify(user_tags_set, [{name, Username}, {tags, ConvertedTags}, | ||||
|                                             {user_who_performed_action, ActingUser}]), | ||||
|         notify_user_tags_set(Username, ConvertedTags, ActingUser), | ||||
|         R | ||||
|     catch | ||||
|         throw:{error, {no_such_user, _}} = Error -> | ||||
|  | @ -373,6 +472,11 @@ set_tags(Username, Tags, ActingUser) -> | |||
|             erlang:raise(Class, Error, Stacktrace) | ||||
|     end . | ||||
| 
 | ||||
| notify_user_tags_set(Username, ConvertedTags, ActingUser) -> | ||||
|     rabbit_log:info("Successfully set user tags for user '~s' to ~p", [Username, ConvertedTags]), | ||||
|     rabbit_event:notify(user_tags_set, [{name, Username}, {tags, ConvertedTags}, | ||||
|                                         {user_who_performed_action, ActingUser}]). | ||||
| 
 | ||||
| -spec set_permissions | ||||
|         (rabbit_types:username(), rabbit_types:vhost(), regexp(), regexp(), | ||||
|          regexp(), rabbit_types:username()) -> | ||||
|  | @ -648,13 +752,27 @@ put_user(User, Version, ActingUser) -> | |||
|                 rabbit_credential_validation:validate(Username, Password) =:= ok | ||||
|         end, | ||||
| 
 | ||||
|     Limits = case rabbit_feature_flags:is_enabled(user_limits) of | ||||
|                  false -> | ||||
|                      undefined; | ||||
|                  true -> | ||||
|                      case maps:get(limits, User, undefined) of | ||||
|                          undefined -> | ||||
|                              undefined; | ||||
|                          Term -> | ||||
|                              case validate_user_limits(Term) of | ||||
|                                  ok -> Term; | ||||
|                                  Error -> throw(Error) | ||||
|                              end | ||||
|                      end | ||||
|              end, | ||||
|     case exists(Username) of | ||||
|         true  -> | ||||
|             case {HasPassword, HasPasswordHash} of | ||||
|                 {true, false} -> | ||||
|                     update_user_password(PassedCredentialValidation, Username, Password, Tags, ActingUser); | ||||
|                     update_user_password(PassedCredentialValidation, Username, Password, Tags, Limits, ActingUser); | ||||
|                 {false, true} -> | ||||
|                     update_user_password_hash(Username, PasswordHash, Tags, User, Version, ActingUser); | ||||
|                     update_user_password_hash(Username, PasswordHash, Tags, Limits, User, Version); | ||||
|                 {true, true} -> | ||||
|                     throw({error, both_password_and_password_hash_are_provided}); | ||||
|                 %% clear password, update tags if needed | ||||
|  | @ -665,63 +783,54 @@ put_user(User, Version, ActingUser) -> | |||
|         false -> | ||||
|             case {HasPassword, HasPasswordHash} of | ||||
|                 {true, false}  -> | ||||
|                     create_user_with_password(PassedCredentialValidation, Username, Password, Tags, Permissions, ActingUser); | ||||
|                     create_user_with_password(PassedCredentialValidation, Username, Password, Tags, Permissions, Limits, ActingUser); | ||||
|                 {false, true}  -> | ||||
|                     create_user_with_password_hash(Username, PasswordHash, Tags, User, Version, Permissions, ActingUser); | ||||
|                     create_user_with_password_hash(Username, PasswordHash, Tags, User, Version, Permissions, Limits, ActingUser); | ||||
|                 {true, true}   -> | ||||
|                     throw({error, both_password_and_password_hash_are_provided}); | ||||
|                 {false, false} -> | ||||
|                     %% this user won't be able to sign in using | ||||
|                     %% a username/password pair but can be used for x509 certificate authentication, | ||||
|                     %% with authn backends such as HTTP or LDAP and so on. | ||||
|                     create_user_with_password(PassedCredentialValidation, Username, <<"">>, Tags, Permissions, ActingUser) | ||||
|                     create_user_with_password(PassedCredentialValidation, Username, <<"">>, Tags, Permissions, Limits, ActingUser) | ||||
|             end | ||||
|     end. | ||||
| 
 | ||||
| update_user_password(_PassedCredentialValidation = true,  Username, Password, Tags, ActingUser) -> | ||||
|     rabbit_auth_backend_internal:change_password(Username, Password, ActingUser), | ||||
|     rabbit_auth_backend_internal:set_tags(Username, Tags, ActingUser); | ||||
| update_user_password(_PassedCredentialValidation = false, _Username, _Password, _Tags, _ActingUser) -> | ||||
| update_user_password(_PassedCredentialValidation = true,  Username, Password, Tags, Limits, ActingUser) -> | ||||
|     %% change_password, set_tags and limits | ||||
|     rabbit_auth_backend_internal:update_user(Username, Password, Tags, Limits, ActingUser); | ||||
| update_user_password(_PassedCredentialValidation = false, _Username, _Password, _Tags, _Limits, _ActingUser) -> | ||||
|     %% we don't log here because | ||||
|     %% rabbit_auth_backend_internal will do it | ||||
|     throw({error, credential_validation_failed}). | ||||
| 
 | ||||
| update_user_password_hash(Username, PasswordHash, Tags, User, Version, ActingUser) -> | ||||
| update_user_password_hash(Username, PasswordHash, Tags, Limits, User, Version) -> | ||||
|     %% when a hash this provided, credential validation | ||||
|     %% is not applied | ||||
|     HashingAlgorithm = hashing_algorithm(User, Version), | ||||
| 
 | ||||
|     Hash = rabbit_misc:b64decode_or_throw(PasswordHash), | ||||
|     rabbit_auth_backend_internal:change_password_hash( | ||||
|       Username, Hash, HashingAlgorithm), | ||||
|     rabbit_auth_backend_internal:set_tags(Username, Tags, ActingUser). | ||||
|     ConvertedTags = [rabbit_data_coercion:to_atom(I) || I <- Tags], | ||||
|     rabbit_auth_backend_internal:update_user_with_hash( | ||||
|       Username, Hash, HashingAlgorithm, ConvertedTags, Limits). | ||||
| 
 | ||||
| create_user_with_password(_PassedCredentialValidation = true,  Username, Password, Tags, undefined, ActingUser) -> | ||||
|     rabbit_auth_backend_internal:add_user(Username, Password, ActingUser), | ||||
|     rabbit_auth_backend_internal:set_tags(Username, Tags, ActingUser); | ||||
| create_user_with_password(_PassedCredentialValidation = true,  Username, Password, Tags, PreconfiguredPermissions, ActingUser) -> | ||||
|     rabbit_auth_backend_internal:add_user(Username, Password, ActingUser), | ||||
|     rabbit_auth_backend_internal:set_tags(Username, Tags, ActingUser), | ||||
| create_user_with_password(_PassedCredentialValidation = true,  Username, Password, Tags, undefined, Limits, ActingUser) -> | ||||
|     rabbit_auth_backend_internal:add_user(Username, Password, ActingUser, Limits, Tags); | ||||
| create_user_with_password(_PassedCredentialValidation = true,  Username, Password, Tags, PreconfiguredPermissions, Limits, ActingUser) -> | ||||
|     rabbit_auth_backend_internal:add_user(Username, Password, ActingUser, Limits, Tags), | ||||
|     preconfigure_permissions(Username, PreconfiguredPermissions, ActingUser); | ||||
| create_user_with_password(_PassedCredentialValidation = false, _Username, _Password, _Tags, _, _) -> | ||||
| create_user_with_password(_PassedCredentialValidation = false, _Username, _Password, _Tags, _, _, _) -> | ||||
|     %% we don't log here because | ||||
|     %% rabbit_auth_backend_internal will do it | ||||
|     throw({error, credential_validation_failed}). | ||||
| 
 | ||||
| create_user_with_password_hash(Username, PasswordHash, Tags, User, Version, PreconfiguredPermissions, ActingUser) -> | ||||
| create_user_with_password_hash(Username, PasswordHash, Tags, User, Version, PreconfiguredPermissions, Limits, ActingUser) -> | ||||
|     %% when a hash this provided, credential validation | ||||
|     %% is not applied | ||||
|     HashingAlgorithm = hashing_algorithm(User, Version), | ||||
|     Hash             = rabbit_misc:b64decode_or_throw(PasswordHash), | ||||
| 
 | ||||
|     %% first we create a user with dummy credentials and no | ||||
|     %% validation applied, then we update password hash | ||||
|     TmpPassword = rabbit_guid:binary(rabbit_guid:gen_secure(), "tmp"), | ||||
|     rabbit_auth_backend_internal:add_user_sans_validation(Username, TmpPassword, ActingUser), | ||||
| 
 | ||||
|     rabbit_auth_backend_internal:change_password_hash( | ||||
|       Username, Hash, HashingAlgorithm), | ||||
|     rabbit_auth_backend_internal:set_tags(Username, Tags, ActingUser), | ||||
|     rabbit_auth_backend_internal:add_user_sans_validation(Username, Hash, HashingAlgorithm, Tags, Limits, ActingUser), | ||||
|     preconfigure_permissions(Username, PreconfiguredPermissions, ActingUser). | ||||
| 
 | ||||
| preconfigure_permissions(_Username, undefined, _ActingUser) -> | ||||
|  | @ -756,8 +865,7 @@ set_user_limits(Username, Definition, ActingUser) when is_map(Definition) -> | |||
|     end. | ||||
| 
 | ||||
| validate_parameters_and_update_limit(Username, Term, ActingUser) -> | ||||
|     case flatten_errors(rabbit_parameter_validation:proplist( | ||||
|                         <<"user-limits">>, user_limit_validation(), Term)) of | ||||
|     case validate_user_limits(Term) of | ||||
|         ok -> | ||||
|             update_user(Username, fun(User) -> | ||||
|                                       internal_user:update_limits(add, User, Term) | ||||
|  | @ -767,6 +875,10 @@ validate_parameters_and_update_limit(Username, Term, ActingUser) -> | |||
|             {error_string, rabbit_misc:format(Reason, Arguments)} | ||||
|     end. | ||||
| 
 | ||||
| validate_user_limits(Term) -> | ||||
|     flatten_errors(rabbit_parameter_validation:proplist( | ||||
|                      <<"user-limits">>, user_limit_validation(), Term)). | ||||
| 
 | ||||
| user_limit_validation() -> | ||||
|     [{<<"max-connections">>, fun rabbit_parameter_validation:integer/2, optional}, | ||||
|      {<<"max-channels">>, fun rabbit_parameter_validation:integer/2, optional}]. | ||||
|  |  | |||
|  | @ -1306,7 +1306,7 @@ handle_method(#'basic.publish'{exchange    = ExchangeNameBin, | |||
|     check_expiration_header(Props), | ||||
|     DoConfirm = Tx =/= none orelse ConfirmEnabled, | ||||
|     {MsgSeqNo, State1} = | ||||
|         case DoConfirm orelse Mandatory of | ||||
|         case DoConfirm of | ||||
|             false -> {undefined, State0}; | ||||
|             true  -> rabbit_global_counters:messages_received_confirm(amqp091, 1), | ||||
|                      SeqNo = State0#ch.publish_seqno, | ||||
|  |  | |||
|  | @ -464,6 +464,10 @@ add_policy(Param, Username) -> | |||
| 
 | ||||
| add_policy(VHost, Param, Username) -> | ||||
|     Key   = maps:get(name,  Param, undefined), | ||||
|     case Key of | ||||
|       undefined -> exit(rabbit_misc:format("policy in virtual host '~s' has undefined name", [VHost])); | ||||
|       _ -> ok | ||||
|     end, | ||||
|     case rabbit_policy:set( | ||||
|            VHost, Key, maps:get(pattern, Param, undefined), | ||||
|            case maps:get(definition, Param, undefined) of | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ | |||
|          get_disk_free/0, set_enabled/1]). | ||||
| 
 | ||||
| -define(SERVER, ?MODULE). | ||||
| -define(ETS_NAME, ?MODULE). | ||||
| -define(DEFAULT_MIN_DISK_CHECK_INTERVAL, 100). | ||||
| -define(DEFAULT_MAX_DISK_CHECK_INTERVAL, 10000). | ||||
| -define(DEFAULT_DISK_FREE_LIMIT, 50000000). | ||||
|  | @ -73,51 +74,42 @@ | |||
| %%---------------------------------------------------------------------------- | ||||
| 
 | ||||
| -spec get_disk_free_limit() -> integer(). | ||||
| 
 | ||||
| get_disk_free_limit() -> | ||||
|     gen_server:call(?MODULE, get_disk_free_limit, infinity). | ||||
|     safe_ets_lookup(disk_free_limit, ?DEFAULT_DISK_FREE_LIMIT). | ||||
| 
 | ||||
| -spec set_disk_free_limit(disk_free_limit()) -> 'ok'. | ||||
| 
 | ||||
| set_disk_free_limit(Limit) -> | ||||
|     gen_server:call(?MODULE, {set_disk_free_limit, Limit}, infinity). | ||||
|     gen_server:call(?MODULE, {set_disk_free_limit, Limit}). | ||||
| 
 | ||||
| -spec get_min_check_interval() -> integer(). | ||||
| 
 | ||||
| get_min_check_interval() -> | ||||
|     gen_server:call(?MODULE, get_min_check_interval, infinity). | ||||
|     safe_ets_lookup(min_check_interval, ?DEFAULT_MIN_DISK_CHECK_INTERVAL). | ||||
| 
 | ||||
| -spec set_min_check_interval(integer()) -> 'ok'. | ||||
| 
 | ||||
| set_min_check_interval(Interval) -> | ||||
|     gen_server:call(?MODULE, {set_min_check_interval, Interval}, infinity). | ||||
|     gen_server:call(?MODULE, {set_min_check_interval, Interval}). | ||||
| 
 | ||||
| -spec get_max_check_interval() -> integer(). | ||||
| 
 | ||||
| get_max_check_interval() -> | ||||
|     gen_server:call(?MODULE, get_max_check_interval, infinity). | ||||
|     safe_ets_lookup(max_check_interval, ?DEFAULT_MAX_DISK_CHECK_INTERVAL). | ||||
| 
 | ||||
| -spec set_max_check_interval(integer()) -> 'ok'. | ||||
| 
 | ||||
| set_max_check_interval(Interval) -> | ||||
|     gen_server:call(?MODULE, {set_max_check_interval, Interval}, infinity). | ||||
|     gen_server:call(?MODULE, {set_max_check_interval, Interval}). | ||||
| 
 | ||||
| -spec get_disk_free() -> (integer() | 'unknown'). | ||||
| 
 | ||||
| get_disk_free() -> | ||||
|     gen_server:call(?MODULE, get_disk_free, infinity). | ||||
|     safe_ets_lookup(disk_free, unknown). | ||||
| 
 | ||||
| -spec set_enabled(string()) -> 'ok'. | ||||
| 
 | ||||
| set_enabled(Enabled) -> | ||||
|     gen_server:call(?MODULE, {set_enabled, Enabled}, infinity). | ||||
|     gen_server:call(?MODULE, {set_enabled, Enabled}). | ||||
| 
 | ||||
| %%---------------------------------------------------------------------------- | ||||
| %% gen_server callbacks | ||||
| %%---------------------------------------------------------------------------- | ||||
| 
 | ||||
| -spec start_link(disk_free_limit()) -> rabbit_types:ok_pid_or_error(). | ||||
| 
 | ||||
| start_link(Args) -> | ||||
|     gen_server:start_link({local, ?SERVER}, ?MODULE, [Args], []). | ||||
| 
 | ||||
|  | @ -125,18 +117,16 @@ init([Limit]) -> | |||
|     Dir = dir(), | ||||
|     {ok, Retries} = application:get_env(rabbit, disk_monitor_failure_retries), | ||||
|     {ok, Interval} = application:get_env(rabbit, disk_monitor_failure_retry_interval), | ||||
|     State = #state{dir          = Dir, | ||||
|                    min_interval = ?DEFAULT_MIN_DISK_CHECK_INTERVAL, | ||||
|                    max_interval = ?DEFAULT_MAX_DISK_CHECK_INTERVAL, | ||||
|                    alarmed      = false, | ||||
|                    enabled      = true, | ||||
|                    limit        = Limit, | ||||
|                    retries      = Retries, | ||||
|                    interval     = Interval}, | ||||
|     {ok, enable(State)}. | ||||
| 
 | ||||
| handle_call(get_disk_free_limit, _From, State = #state{limit = Limit}) -> | ||||
|     {reply, Limit, State}; | ||||
|     ?ETS_NAME = ets:new(?ETS_NAME, [protected, set, named_table]), | ||||
|     State0 = #state{dir          = Dir, | ||||
|                     alarmed      = false, | ||||
|                     enabled      = true, | ||||
|                     limit        = Limit, | ||||
|                     retries      = Retries, | ||||
|                     interval     = Interval}, | ||||
|     State1 = set_min_check_interval(?DEFAULT_MIN_DISK_CHECK_INTERVAL, State0), | ||||
|     State2 = set_max_check_interval(?DEFAULT_MAX_DISK_CHECK_INTERVAL, State1), | ||||
|     {ok, enable(State2)}. | ||||
| 
 | ||||
| handle_call({set_disk_free_limit, _}, _From, #state{enabled = false} = State) -> | ||||
|     rabbit_log:info("Cannot set disk free limit: " | ||||
|  | @ -146,20 +136,14 @@ handle_call({set_disk_free_limit, _}, _From, #state{enabled = false} = State) -> | |||
| handle_call({set_disk_free_limit, Limit}, _From, State) -> | ||||
|     {reply, ok, set_disk_limits(State, Limit)}; | ||||
| 
 | ||||
| handle_call(get_min_check_interval, _From, State) -> | ||||
|     {reply, State#state.min_interval, State}; | ||||
| 
 | ||||
| handle_call(get_max_check_interval, _From, State) -> | ||||
|     {reply, State#state.max_interval, State}; | ||||
| 
 | ||||
| handle_call({set_min_check_interval, MinInterval}, _From, State) -> | ||||
|     {reply, ok, State#state{min_interval = MinInterval}}; | ||||
|     {reply, ok, set_min_check_interval(MinInterval, State)}; | ||||
| 
 | ||||
| handle_call({set_max_check_interval, MaxInterval}, _From, State) -> | ||||
|     {reply, ok, State#state{max_interval = MaxInterval}}; | ||||
| 
 | ||||
| handle_call(get_disk_free, _From, State = #state { actual = Actual }) -> | ||||
|     {reply, Actual, State}; | ||||
|     {reply, ok, set_max_check_interval(MaxInterval, State)}; | ||||
| 
 | ||||
| handle_call({set_enabled, _Enabled = true}, _From, State) -> | ||||
|     start_timer(set_disk_limits(State, State#state.limit)), | ||||
|  | @ -194,14 +178,36 @@ code_change(_OldVsn, State, _Extra) -> | |||
| %% Server Internals | ||||
| %%---------------------------------------------------------------------------- | ||||
| 
 | ||||
| safe_ets_lookup(Key, Default) -> | ||||
|     try | ||||
|         case ets:lookup(?ETS_NAME, Key) of | ||||
|             [{Key, Value}] -> | ||||
|                 Value; | ||||
|             [] -> | ||||
|                 Default | ||||
|         end | ||||
|     catch | ||||
|         error:badarg -> | ||||
|             Default | ||||
|     end. | ||||
| 
 | ||||
| % the partition / drive containing this directory will be monitored | ||||
| dir() -> rabbit_mnesia:dir(). | ||||
| 
 | ||||
| set_min_check_interval(MinInterval, State) -> | ||||
|     ets:insert(?ETS_NAME, {min_check_interval, MinInterval}), | ||||
|     State#state{min_interval = MinInterval}. | ||||
| 
 | ||||
| set_max_check_interval(MaxInterval, State) -> | ||||
|     ets:insert(?ETS_NAME, {max_check_interval, MaxInterval}), | ||||
|     State#state{max_interval = MaxInterval}. | ||||
| 
 | ||||
| set_disk_limits(State, Limit0) -> | ||||
|     Limit = interpret_limit(Limit0), | ||||
|     State1 = State#state { limit = Limit }, | ||||
|     rabbit_log:info("Disk free limit set to ~pMB", | ||||
|                     [trunc(Limit / 1000000)]), | ||||
|     ets:insert(?ETS_NAME, {disk_free_limit, Limit}), | ||||
|     internal_update(State1). | ||||
| 
 | ||||
| internal_update(State = #state { limit   = Limit, | ||||
|  | @ -219,7 +225,8 @@ internal_update(State = #state { limit   = Limit, | |||
|         _ -> | ||||
|             ok | ||||
|     end, | ||||
|     State #state {alarmed = NewAlarmed, actual = CurrentFree}. | ||||
|     ets:insert(?ETS_NAME, {disk_free, CurrentFree}), | ||||
|     State#state{alarmed = NewAlarmed, actual = CurrentFree}. | ||||
| 
 | ||||
| get_disk_free(Dir) -> | ||||
|     get_disk_free(Dir, os:type()). | ||||
|  | @ -227,11 +234,89 @@ get_disk_free(Dir) -> | |||
| get_disk_free(Dir, {unix, Sun}) | ||||
|   when Sun =:= sunos; Sun =:= sunos4; Sun =:= solaris -> | ||||
|     Df = os:find_executable("df"), | ||||
|     parse_free_unix(rabbit_misc:os_cmd(Df ++ " -k " ++ Dir)); | ||||
|     parse_free_unix(run_cmd(Df ++ " -k " ++ Dir)); | ||||
| get_disk_free(Dir, {unix, _}) -> | ||||
|     Df = os:find_executable("df"), | ||||
|     parse_free_unix(rabbit_misc:os_cmd(Df ++ " -kP " ++ Dir)); | ||||
|     parse_free_unix(run_cmd(Df ++ " -kP " ++ Dir)); | ||||
| get_disk_free(Dir, {win32, _}) -> | ||||
|     % Dir: | ||||
|     % "c:/Users/username/AppData/Roaming/RabbitMQ/db/rabbit2@username-z01-mnesia" | ||||
|     case win32_get_drive_letter(Dir) of | ||||
|         error -> | ||||
|             rabbit_log:warning("Expected the mnesia directory absolute " | ||||
|                                "path to start with a drive letter like " | ||||
|                                "'C:'. The path is: '~p'", [Dir]), | ||||
|             case win32_get_disk_free_dir(Dir) of | ||||
|                 {ok, Free} -> | ||||
|                     Free; | ||||
|                 _ -> exit(could_not_determine_disk_free) | ||||
|             end; | ||||
|         DriveLetter -> | ||||
|             case win32_get_disk_free_fsutil(DriveLetter) of | ||||
|                 {ok, Free0} -> Free0; | ||||
|                 error -> | ||||
|                     case win32_get_disk_free_pwsh(DriveLetter) of | ||||
|                         {ok, Free1} -> Free1; | ||||
|                         _ -> exit(could_not_determine_disk_free) | ||||
|                     end | ||||
|             end | ||||
|     end. | ||||
| 
 | ||||
| parse_free_unix(Str) -> | ||||
|     case string:tokens(Str, "\n") of | ||||
|         [_, S | _] -> case string:tokens(S, " \t") of | ||||
|                           [_, _, _, Free | _] -> list_to_integer(Free) * 1024; | ||||
|                           _                   -> exit({unparseable, Str}) | ||||
|                       end; | ||||
|         _          -> exit({unparseable, Str}) | ||||
|     end. | ||||
| 
 | ||||
| win32_get_drive_letter([DriveLetter, $:, $/ | _]) when | ||||
|       (DriveLetter >= $a andalso DriveLetter =< $z) orelse | ||||
|       (DriveLetter >= $A andalso DriveLetter =< $Z) -> | ||||
|     DriveLetter; | ||||
| win32_get_drive_letter(_) -> | ||||
|     error. | ||||
| 
 | ||||
| win32_get_disk_free_fsutil(DriveLetter) when | ||||
|       (DriveLetter >= $a andalso DriveLetter =< $z) orelse | ||||
|       (DriveLetter >= $A andalso DriveLetter =< $Z) -> | ||||
|     % DriveLetter $c | ||||
|     FsutilCmd = "fsutil.exe volume diskfree " ++ [DriveLetter] ++ ":", | ||||
| 
 | ||||
|     % C:\windows\system32>fsutil volume diskfree c: | ||||
|     % Total free bytes        :   812,733,878,272 (756.9 GB) | ||||
|     % Total bytes             : 1,013,310,287,872 (943.7 GB) | ||||
|     % Total quota free bytes  :   812,733,878,272 (756.9 GB) | ||||
|     case run_cmd(FsutilCmd) of | ||||
|         {error, timeout} -> | ||||
|             error; | ||||
|         FsutilResult -> | ||||
|             case string:slice(FsutilResult, 0, 5) of | ||||
|                 "Error" -> | ||||
|                     error; | ||||
|                 "Total" -> | ||||
|                     FirstLine = hd(string:tokens(FsutilResult, "\r\n")), | ||||
|                     {match, [FreeStr]} = re:run(FirstLine, "(\\d+,?)+", [{capture, first, list}]), | ||||
|                     {ok, list_to_integer(lists:flatten(string:tokens(FreeStr, ",")))} | ||||
|             end | ||||
|     end. | ||||
| 
 | ||||
| win32_get_disk_free_pwsh(DriveLetter) when | ||||
|       (DriveLetter >= $a andalso DriveLetter =< $z) orelse | ||||
|       (DriveLetter >= $A andalso DriveLetter =< $Z) -> | ||||
|     % DriveLetter $c | ||||
|     PoshCmd = "powershell.exe -NoLogo -NoProfile -NonInteractive -Command (Get-PSDrive " ++ [DriveLetter] ++ ").Free", | ||||
|     case run_cmd(PoshCmd) of | ||||
|         {error, timeout} -> | ||||
|             error; | ||||
|         PoshResultStr -> | ||||
|             % Note: remove \r\n | ||||
|             PoshResult = string:slice(PoshResultStr, 0, length(PoshResultStr) - 2), | ||||
|             {ok, list_to_integer(PoshResult)} | ||||
|     end. | ||||
| 
 | ||||
| win32_get_disk_free_dir(Dir) -> | ||||
|     %% On Windows, the Win32 API enforces a limit of 260 characters | ||||
|     %% (MAX_PATH). If we call `dir` with a path longer than that, it | ||||
|     %% fails with "File not found". Starting with Windows 10 version | ||||
|  | @ -253,22 +338,11 @@ get_disk_free(Dir, {win32, _}) -> | |||
|     %% See the following page to learn more about this: | ||||
|     %% https://ss64.com/nt/syntax-filenames.html | ||||
|     RawDir = "\\\\?\\" ++ string:replace(Dir, "/", "\\", all), | ||||
|     parse_free_win32(rabbit_misc:os_cmd("dir /-C /W \"" ++ RawDir ++ "\"")). | ||||
| 
 | ||||
| parse_free_unix(Str) -> | ||||
|     case string:tokens(Str, "\n") of | ||||
|         [_, S | _] -> case string:tokens(S, " \t") of | ||||
|                           [_, _, _, Free | _] -> list_to_integer(Free) * 1024; | ||||
|                           _                   -> exit({unparseable, Str}) | ||||
|                       end; | ||||
|         _          -> exit({unparseable, Str}) | ||||
|     end. | ||||
| 
 | ||||
| parse_free_win32(CommandResult) -> | ||||
|     CommandResult = run_cmd("dir /-C /W \"" ++ RawDir ++ "\""), | ||||
|     LastLine = lists:last(string:tokens(CommandResult, "\r\n")), | ||||
|     {match, [Free]} = re:run(lists:reverse(LastLine), "(\\d+)", | ||||
|                              [{capture, all_but_first, list}]), | ||||
|     list_to_integer(lists:reverse(Free)). | ||||
|     {ok, list_to_integer(lists:reverse(Free))}. | ||||
| 
 | ||||
| interpret_limit({mem_relative, Relative}) | ||||
|     when is_number(Relative) -> | ||||
|  | @ -318,3 +392,20 @@ enable(#state{dir = Dir, interval = Interval, limit = Limit, retries = Retries} | |||
|             erlang:send_after(Interval, self(), try_enable), | ||||
|             State#state{enabled = false} | ||||
|     end. | ||||
| 
 | ||||
| run_cmd(Cmd) -> | ||||
|     Pid = self(), | ||||
|     Ref = make_ref(), | ||||
|     CmdFun = fun() -> | ||||
|         CmdResult = rabbit_misc:os_cmd(Cmd), | ||||
|         Pid ! {Pid, Ref, CmdResult} | ||||
|     end, | ||||
|     CmdPid = spawn(CmdFun), | ||||
|     receive | ||||
|         {Pid, Ref, CmdResult} -> | ||||
|             CmdResult | ||||
|     after 5000 -> | ||||
|         exit(CmdPid, kill), | ||||
|         rabbit_log:error("Command timed out: '~s'", [Cmd]), | ||||
|         {error, timeout} | ||||
|     end. | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ is_file(File) -> | |||
| 
 | ||||
| is_dir(Dir) -> is_dir_internal(read_file_info(Dir)). | ||||
| 
 | ||||
| is_dir_no_handle(Dir) -> is_dir_internal(prim_file:read_file_info(Dir)). | ||||
| is_dir_no_handle(Dir) -> is_dir_internal(file:read_file_info(Dir, [raw])). | ||||
| 
 | ||||
| is_dir_internal({ok, #file_info{type=directory}}) -> true; | ||||
| is_dir_internal(_)                                -> false. | ||||
|  | @ -83,14 +83,23 @@ wildcard(Pattern, Dir) -> | |||
| list_dir(Dir) -> with_handle(fun () -> prim_file:list_dir(Dir) end). | ||||
| 
 | ||||
| read_file_info(File) -> | ||||
|     with_handle(fun () -> prim_file:read_file_info(File) end). | ||||
|     with_handle(fun () -> file:read_file_info(File, [raw]) end). | ||||
| 
 | ||||
| -spec read_term_file | ||||
|         (file:filename()) -> {'ok', [any()]} | rabbit_types:error(any()). | ||||
| 
 | ||||
| read_term_file(File) -> | ||||
|     try | ||||
|         {ok, Data} = with_handle(fun () -> prim_file:read_file(File) end), | ||||
|         F = fun() -> | ||||
|                     {ok, FInfo} = file:read_file_info(File, [raw]), | ||||
|                     {ok, Fd} = file:open(File, [read, raw, binary]), | ||||
|                     try | ||||
|                         file:read(Fd, FInfo#file_info.size) | ||||
|                     after | ||||
|                         file:close(Fd) | ||||
|                     end | ||||
|             end, | ||||
|         {ok, Data} = with_handle(F), | ||||
|         {ok, Tokens, _} = erl_scan:string(binary_to_list(Data)), | ||||
|         TokenGroups = group_tokens(Tokens), | ||||
|         {ok, [begin | ||||
|  |  | |||
|  | @ -64,7 +64,11 @@ node_health_check(rabbit_node_monitor) -> | |||
|     end; | ||||
| 
 | ||||
| node_health_check(alarms) -> | ||||
|     case proplists:get_value(alarms, rabbit:status()) of | ||||
|     % Note: | ||||
|     % Removed call to rabbit:status/0 here due to a memory leak on win32, | ||||
|     % plus it uses an excessive amount of resources | ||||
|     % Alternative to https://github.com/rabbitmq/rabbitmq-server/pull/3893 | ||||
|     case rabbit:alarms() of | ||||
|         [] -> | ||||
|             ok; | ||||
|         Alarms -> | ||||
|  |  | |||
|  | @ -78,6 +78,8 @@ handle_info(tick, #state{timeout = Timeout} = State) -> | |||
|                               %% down `rabbit_sup` and the whole `rabbit` app. | ||||
|                               [] | ||||
|                       end, | ||||
| 
 | ||||
| 
 | ||||
|               rabbit_core_metrics:queue_stats(QName, Infos), | ||||
|               rabbit_event:notify(queue_stats, Infos ++ [{name, QName}, | ||||
|                                                          {messages, COffs}, | ||||
|  |  | |||
|  | @ -176,4 +176,6 @@ merge_policy_value(<<"max-length-bytes">>, Val, OpVal) -> min(Val, OpVal); | |||
| merge_policy_value(<<"max-in-memory-length">>, Val, OpVal) -> min(Val, OpVal); | ||||
| merge_policy_value(<<"max-in-memory-bytes">>, Val, OpVal) -> min(Val, OpVal); | ||||
| merge_policy_value(<<"expires">>, Val, OpVal)          -> min(Val, OpVal); | ||||
| merge_policy_value(<<"delivery-limit">>, Val, OpVal)   -> min(Val, OpVal). | ||||
| merge_policy_value(<<"delivery-limit">>, Val, OpVal)   -> min(Val, OpVal); | ||||
| %% use operator policy value for booleans | ||||
| merge_policy_value(_Key, Val, OpVal) when is_boolean(Val) andalso is_boolean(OpVal) -> OpVal. | ||||
|  |  | |||
|  | @ -846,18 +846,22 @@ phase_update_mnesia(StreamId, Args, #{reference := QName, | |||
|                     %% This can happen during recovery | ||||
|                     %% we need to re-initialise the queue record | ||||
|                     %% if the stream id is a match | ||||
|                     [Q] = mnesia:dirty_read(rabbit_durable_queue, QName), | ||||
|                     case amqqueue:get_type_state(Q) of | ||||
|                         #{name := S} when S == StreamId -> | ||||
|                             rabbit_log:debug("~s: initializing queue record for stream id  ~s", | ||||
|                                              [?MODULE, StreamId]), | ||||
|                             _ = rabbit_amqqueue:ensure_rabbit_queue_record_is_initialized(Fun(Q)), | ||||
|                     case mnesia:dirty_read(rabbit_durable_queue, QName) of | ||||
|                         [] -> | ||||
|                             %% queue not found at all, it must have been deleted | ||||
|                             ok; | ||||
|                         _ -> | ||||
|                             ok | ||||
|                     end, | ||||
| 
 | ||||
|                     send_self_command({mnesia_updated, StreamId, Args}); | ||||
|                         [Q] -> | ||||
|                             case amqqueue:get_type_state(Q) of | ||||
|                                 #{name := S} when S == StreamId -> | ||||
|                                     rabbit_log:debug("~s: initializing queue record for stream id  ~s", | ||||
|                                                      [?MODULE, StreamId]), | ||||
|                                     _ = rabbit_amqqueue:ensure_rabbit_queue_record_is_initialized(Fun(Q)), | ||||
|                                     ok; | ||||
|                                 _ -> | ||||
|                                     ok | ||||
|                             end, | ||||
|                             send_self_command({mnesia_updated, StreamId, Args}) | ||||
|                     end; | ||||
|                 _ -> | ||||
|                     send_self_command({mnesia_updated, StreamId, Args}) | ||||
|             catch _:E -> | ||||
|  |  | |||
|  | @ -46,7 +46,10 @@ groups() -> | |||
|                                import_case13, | ||||
|                                import_case14, | ||||
|                                import_case15, | ||||
|                                import_case16 | ||||
|                                import_case16, | ||||
|                                import_case17, | ||||
|                                import_case18, | ||||
|                                import_case19 | ||||
|                               ]}, | ||||
|          | ||||
|         {boot_time_import_using_classic_source, [], [ | ||||
|  | @ -236,6 +239,36 @@ import_case16(Config) -> | |||
|         {skip, "Should not run in mixed version environments"} | ||||
|     end. | ||||
| 
 | ||||
| import_case17(Config) -> import_invalid_file_case(Config, "failing_case17"). | ||||
| 
 | ||||
| import_case18(Config) -> | ||||
|     case rabbit_ct_helpers:is_mixed_versions() of | ||||
|       false -> | ||||
|         case rabbit_ct_broker_helpers:enable_feature_flag(Config, user_limits) of | ||||
|             ok -> | ||||
|                 import_file_case(Config, "case18"), | ||||
|                 User = <<"limited_guest">>, | ||||
|                 UserIsImported = | ||||
|                     fun () -> | ||||
|                             case user_lookup(Config, User) of | ||||
|                                 {error, not_found} -> false; | ||||
|                                 _       -> true | ||||
|                             end | ||||
|                     end, | ||||
|                 rabbit_ct_helpers:await_condition(UserIsImported, 20000), | ||||
|                 {ok, UserRec} = user_lookup(Config, User), | ||||
|                 ?assertEqual(#{<<"max-connections">> => 2}, internal_user:get_limits(UserRec)), | ||||
|                 ok; | ||||
|             Skip -> | ||||
|                 Skip | ||||
|         end; | ||||
|       _ -> | ||||
|         %% skip the test in mixed version mode | ||||
|         {skip, "Should not run in mixed version environments"} | ||||
|     end. | ||||
| 
 | ||||
| import_case19(Config) -> import_invalid_file_case(Config, "failing_case19"). | ||||
| 
 | ||||
| export_import_round_trip_case1(Config) -> | ||||
|     case rabbit_ct_helpers:is_mixed_versions() of | ||||
|       false -> | ||||
|  | @ -382,3 +415,6 @@ queue_lookup(Config, VHost, Name) -> | |||
| 
 | ||||
| vhost_lookup(Config, VHost) -> | ||||
|     rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_vhost, lookup, [VHost]). | ||||
| 
 | ||||
| user_lookup(Config, User) -> | ||||
|     rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, lookup_user, [User]). | ||||
|  |  | |||
|  | @ -0,0 +1,46 @@ | |||
| { | ||||
|   "bindings": [], | ||||
|   "exchanges": [], | ||||
|   "global_parameters": [ | ||||
|     { | ||||
|       "name": "cluster_name", | ||||
|       "value": "rabbitmq@localhost" | ||||
|     } | ||||
|   ], | ||||
|   "parameters": [], | ||||
|   "permissions": [ | ||||
|     { | ||||
|       "configure": ".*", | ||||
|       "read": ".*", | ||||
|       "user": "guest", | ||||
|       "vhost": "/", | ||||
|       "write": ".*" | ||||
|     } | ||||
|   ], | ||||
|   "policies": [], | ||||
|   "queues": [], | ||||
|   "rabbit_version": "3.9.1", | ||||
|   "rabbitmq_version": "3.9.1", | ||||
|   "topic_permissions": [], | ||||
|   "users": [ | ||||
|     { | ||||
|       "hashing_algorithm": "rabbit_password_hashing_sha256", | ||||
|       "limits": {"max-connections" : 2}, | ||||
|       "name": "limited_guest", | ||||
|       "password_hash": "wS4AT3B4Z5RpWlFn1FA30osf2C75D7WA3gem591ACDZ6saO6", | ||||
|       "tags": [ | ||||
|         "administrator" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "vhosts": [ | ||||
|     { | ||||
|       "limits": [], | ||||
|       "name": "/" | ||||
|     }, | ||||
|     { | ||||
|       "limits": [], | ||||
|       "name": "tagged" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -0,0 +1,19 @@ | |||
| { | ||||
|   "vhosts": [ | ||||
|     { | ||||
|       "name": "\/" | ||||
|     } | ||||
|   ], | ||||
|   "policies": [ | ||||
|     { | ||||
|       "vhost": "\/", | ||||
|       "pattern": "^project-nd-ns-", | ||||
|       "apply-to": "queues", | ||||
|       "definition": { | ||||
|         "expires": 120000, | ||||
|         "max-length": 10000 | ||||
|       }, | ||||
|       "priority": 1 | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -0,0 +1,46 @@ | |||
| { | ||||
|   "bindings": [], | ||||
|   "exchanges": [], | ||||
|   "global_parameters": [ | ||||
|     { | ||||
|       "name": "cluster_name", | ||||
|       "value": "rabbitmq@localhost" | ||||
|     } | ||||
|   ], | ||||
|   "parameters": [], | ||||
|   "permissions": [ | ||||
|     { | ||||
|       "configure": ".*", | ||||
|       "read": ".*", | ||||
|       "user": "guest", | ||||
|       "vhost": "/", | ||||
|       "write": ".*" | ||||
|     } | ||||
|   ], | ||||
|   "policies": [], | ||||
|   "queues": [], | ||||
|   "rabbit_version": "3.9.1", | ||||
|   "rabbitmq_version": "3.9.1", | ||||
|   "topic_permissions": [], | ||||
|   "users": [ | ||||
|     { | ||||
|       "hashing_algorithm": "rabbit_password_hashing_sha256", | ||||
|       "limits": {"max-connections" : "twomincepies"}, | ||||
|       "name": "limited_guest", | ||||
|       "password_hash": "wS4AT3B4Z5RpWlFn1FA30osf2C75D7WA3gem591ACDZ6saO6", | ||||
|       "tags": [ | ||||
|         "administrator" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "vhosts": [ | ||||
|     { | ||||
|       "limits": [], | ||||
|       "name": "/" | ||||
|     }, | ||||
|     { | ||||
|       "limits": [], | ||||
|       "name": "tagged" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -29,6 +29,7 @@ groups() -> | |||
|                              confirm_nowait, | ||||
|                              confirm_ack, | ||||
|                              confirm_acks, | ||||
|                              confirm_after_mandatory_bug, | ||||
|                              confirm_mandatory_unroutable, | ||||
|                              confirm_unroutable_message], | ||||
|     [ | ||||
|  | @ -187,6 +188,17 @@ confirm_acks(Config) -> | |||
|     publish(Ch, QName, [<<"msg1">>, <<"msg2">>, <<"msg3">>, <<"msg4">>]), | ||||
|     receive_many(lists:seq(1, 4)). | ||||
| 
 | ||||
| confirm_after_mandatory_bug(Config) -> | ||||
|     {_Conn, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0), | ||||
|     QName = ?config(queue_name, Config), | ||||
|     declare_queue(Ch, Config, QName), | ||||
|     ok = amqp_channel:call(Ch, #'basic.publish'{routing_key = QName, | ||||
|                                                 mandatory = true}, #amqp_msg{payload = <<"msg1">>}), | ||||
|     #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}), | ||||
|     publish(Ch, QName, [<<"msg2">>]), | ||||
|     true = amqp_channel:wait_for_confirms(Ch, 1), | ||||
|     ok. | ||||
| 
 | ||||
| %% For unroutable messages, the broker will issue a confirm once the exchange verifies a message | ||||
| %% won't route to any queue (returns an empty list of queues). | ||||
| %% If the message is also published as mandatory, the basic.return is sent to the client before | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ | |||
| -include_lib("common_test/include/ct.hrl"). | ||||
| -include_lib("eunit/include/eunit.hrl"). | ||||
| -include_lib("rabbit_common/include/rabbit.hrl"). | ||||
| -include("src/rabbit_fifo.hrl"). | ||||
| -include_lib("rabbit/src/rabbit_fifo.hrl"). | ||||
| 
 | ||||
| %%%=================================================================== | ||||
| %%% Common Test callbacks | ||||
|  |  | |||
|  | @ -67,6 +67,12 @@ set_disk_free_limit_command(Config) -> | |||
|       ?MODULE, set_disk_free_limit_command1, [Config]). | ||||
| 
 | ||||
| set_disk_free_limit_command1(_Config) -> | ||||
|     F = fun () -> | ||||
|         DiskFree = rabbit_disk_monitor:get_disk_free(), | ||||
|         DiskFree =/= unknown | ||||
|     end, | ||||
|     rabbit_ct_helpers:await_condition(F), | ||||
| 
 | ||||
|     %% Use an integer | ||||
|     rabbit_disk_monitor:set_disk_free_limit({mem_relative, 1}), | ||||
|     disk_free_limit_to_total_memory_ratio_is(1), | ||||
|  | @ -84,7 +90,8 @@ set_disk_free_limit_command1(_Config) -> | |||
|     passed. | ||||
| 
 | ||||
| disk_free_limit_to_total_memory_ratio_is(MemRatio) -> | ||||
|     DiskFreeLimit = rabbit_disk_monitor:get_disk_free_limit(), | ||||
|     ExpectedLimit = MemRatio * vm_memory_monitor:get_total_memory(), | ||||
|     % Total memory is unstable, so checking order | ||||
|     true = ExpectedLimit/rabbit_disk_monitor:get_disk_free_limit() < 1.2, | ||||
|     true = ExpectedLimit/rabbit_disk_monitor:get_disk_free_limit() > 0.98. | ||||
|     true = ExpectedLimit/DiskFreeLimit < 1.2, | ||||
|     true = ExpectedLimit/DiskFreeLimit > 0.98. | ||||
|  |  | |||
|  | @ -88,7 +88,7 @@ disk_monitor_enable1(_Config) -> | |||
|     application:set_env(rabbit, disk_monitor_failure_retry_interval, 100), | ||||
|     ok = rabbit_sup:stop_child(rabbit_disk_monitor_sup), | ||||
|     ok = rabbit_sup:start_delayed_restartable_child(rabbit_disk_monitor, [1000]), | ||||
|     undefined = rabbit_disk_monitor:get_disk_free(), | ||||
|     unknown = rabbit_disk_monitor:get_disk_free(), | ||||
|     Cmd = case os:type() of | ||||
|               {win32, _} -> " Le volume dans le lecteur C n’a pas de nom.\n" | ||||
|                             " Le numéro de série du volume est 707D-5BDC\n" | ||||
|  |  | |||
|  | @ -21,7 +21,8 @@ all() -> | |||
| groups() -> | ||||
|     [ | ||||
|       {parallel_tests, [parallel], [ | ||||
|           merge_operator_policy_definitions | ||||
|           merge_operator_policy_definitions, | ||||
|           conflict_resolution_for_booleans | ||||
|         ]} | ||||
|     ]. | ||||
| 
 | ||||
|  | @ -102,6 +103,54 @@ merge_operator_policy_definitions(_Config) -> | |||
|                     [{definition, [ | ||||
|                       {<<"message-ttl">>, 3000} | ||||
|                     ]}]) | ||||
|     ), | ||||
|     ). | ||||
| 
 | ||||
|     passed. | ||||
| 
 | ||||
|   conflict_resolution_for_booleans(_Config) -> | ||||
|     ?assertEqual( | ||||
|       [ | ||||
|         {<<"remote-dc-replicate">>, true} | ||||
|       ], | ||||
|       rabbit_policy:merge_operator_definitions( | ||||
|          #{definition => #{ | ||||
|            <<"remote-dc-replicate">> => true | ||||
|          }}, | ||||
|          [{definition, [ | ||||
|            {<<"remote-dc-replicate">>, true} | ||||
|          ]}])), | ||||
| 
 | ||||
|     ?assertEqual( | ||||
|       [ | ||||
|         {<<"remote-dc-replicate">>, false} | ||||
|       ], | ||||
|       rabbit_policy:merge_operator_definitions( | ||||
|         #{definition => #{ | ||||
|           <<"remote-dc-replicate">> => false | ||||
|         }}, | ||||
|         [{definition, [ | ||||
|           {<<"remote-dc-replicate">>, false} | ||||
|         ]}])), | ||||
| 
 | ||||
|     ?assertEqual( | ||||
|       [ | ||||
|         {<<"remote-dc-replicate">>, true} | ||||
|       ], | ||||
|       rabbit_policy:merge_operator_definitions( | ||||
|         #{definition => #{ | ||||
|           <<"remote-dc-replicate">> => false | ||||
|         }}, | ||||
|         [{definition, [ | ||||
|           {<<"remote-dc-replicate">>, true} | ||||
|         ]}])), | ||||
| 
 | ||||
|     ?assertEqual( | ||||
|       [ | ||||
|         {<<"remote-dc-replicate">>, false} | ||||
|       ], | ||||
|       rabbit_policy:merge_operator_definitions( | ||||
|         #{definition => #{ | ||||
|           <<"remote-dc-replicate">> => true | ||||
|         }}, | ||||
|         [{definition, [ | ||||
|           {<<"remote-dc-replicate">>, false} | ||||
|         ]}])). | ||||
|  | @ -139,6 +139,46 @@ In that case, the configuration will look like this: | |||
| 
 | ||||
| NOTE: `jwks_url` takes precedence over `signing_keys` if both are provided. | ||||
| 
 | ||||
| ### Variables Configurable in rabbitmq.conf | ||||
| 
 | ||||
| | Key                                      | Documentation      | ||||
| |------------------------------------------|----------- | ||||
| | `auth_oauth2.resource_server_id`         | [The Resource Server ID](#resource-server-id-and-scope-prefixes) | ||||
| | `auth_oauth2.additional_scopes_key`      | Configure the plugin to also look in other fields (maps to `additional_rabbitmq_scopes` in the old format). | ||||
| | `auth_oauth2.default_key`                | ID of the default signing key. | ||||
| | `auth_oauth2.signing_keys`               | Paths to signing key files. | ||||
| | `auth_oauth2.jwks_url`                   | The URL of key server. According to the [JWT Specification](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.2) key server URL must be https. | ||||
| | `auth_oauth2.https.cacertfile`           | Path to a file containing PEM-encoded CA certificates. The CA certificates are used during key server [peer verification](https://rabbitmq.com/ssl.html#peer-verification). | ||||
| | `auth_oauth2.https.depth`                | The maximum number of non-self-issued intermediate certificates that may follow the peer certificate in a valid [certification path](https://rabbitmq.com/ssl.html#peer-verification-depth). Default is 10. | ||||
| | `auth_oauth2.https.peer_verification`    | Should [peer verification](https://rabbitmq.com/ssl.html#peer-verification) be enabled. Available values: `verify_none`, `verify_peer`. Default is `verify_none`. It is recommended to configure `verify_peer`. Peer verification requires a certain amount of setup and is more secure. | ||||
| | `auth_oauth2.https.fail_if_no_peer_cert` | Used together with `auth_oauth2.https.peer_verification = verify_peer`. When set to `true`, TLS connection will be rejected if client fails to provide a certificate. Default is `false`. | ||||
| | `auth_oauth2.https.hostname_verification`| Enable wildcard-aware hostname verification for key server. Available values: `wildcard`, `none`. Default is `none`. | ||||
| | `auth_oauth2.algorithms`                 | Restrict [the usable algorithms](https://github.com/potatosalad/erlang-jose#algorithm-support). | ||||
| 
 | ||||
| For example: | ||||
| 
 | ||||
| Configure with key files | ||||
| ``` | ||||
| auth_oauth2.resource_server_id = new_resource_server_id | ||||
| auth_oauth2.additional_scopes_key = my_custom_scope_key | ||||
| auth_oauth2.default_key = id1 | ||||
| auth_oauth2.signing_keys.id1 = test/config_schema_SUITE_data/certs/key.pem | ||||
| auth_oauth2.signing_keys.id2 = test/config_schema_SUITE_data/certs/cert.pem | ||||
| auth_oauth2.algorithms.1 = HS256 | ||||
| auth_oauth2.algorithms.2 = RS256 | ||||
| ``` | ||||
| Configure with key server | ||||
| ``` | ||||
| auth_oauth2.resource_server_id = new_resource_server_id | ||||
| auth_oauth2.jwks_url = https://my-jwt-issuer/jwks.json | ||||
| auth_oauth2.https.cacertfile = test/config_schema_SUITE_data/certs/cacert.pem | ||||
| auth_oauth2.https.peer_verification = verify_peer | ||||
| auth_oauth2.https.depth = 5 | ||||
| auth_oauth2.https.fail_if_no_peer_cert = true | ||||
| auth_oauth2.https.hostname_verification = wildcard | ||||
| auth_oauth2.algorithms.1 = HS256 | ||||
| auth_oauth2.algorithms.2 = RS256 | ||||
| ``` | ||||
| ### Resource Server ID and Scope Prefixes | ||||
| 
 | ||||
| OAuth 2.0 (and thus UAA-provided) tokens use scopes to communicate what set of permissions particular | ||||
|  |  | |||
|  | @ -77,3 +77,52 @@ | |||
|                   end, Settings), | ||||
|     maps:from_list(SigningKeys) | ||||
|  end}. | ||||
| 
 | ||||
| {mapping, | ||||
|  "auth_oauth2.jwks_url", | ||||
|  "rabbitmq_auth_backend_oauth2.key_config.jwks_url", | ||||
|  [{datatype, string}, {validators, ["uri", "https_uri"]}]}. | ||||
| 
 | ||||
| {mapping, | ||||
|  "auth_oauth2.https.peer_verification", | ||||
|  "rabbitmq_auth_backend_oauth2.key_config.peer_verification", | ||||
|  [{datatype, {enum, [verify_peer, verify_none]}}]}. | ||||
| 
 | ||||
| {mapping, | ||||
|  "auth_oauth2.https.cacertfile", | ||||
|  "rabbitmq_auth_backend_oauth2.key_config.cacertfile", | ||||
|  [{datatype, file}, {validators, ["file_accessible"]}]}. | ||||
| 
 | ||||
| {mapping, | ||||
|  "auth_oauth2.https.depth", | ||||
|  "rabbitmq_auth_backend_oauth2.key_config.depth", | ||||
|  [{datatype, integer}]}. | ||||
| 
 | ||||
| {mapping, | ||||
|  "auth_oauth2.https.hostname_verification", | ||||
|  "rabbitmq_auth_backend_oauth2.key_config.hostname_verification", | ||||
|  [{datatype, {enum, [wildcard, none]}}]}. | ||||
| 
 | ||||
| {mapping, | ||||
|  "auth_oauth2.https.crl_check", | ||||
|  "rabbitmq_auth_backend_oauth2.key_config.crl_check", | ||||
|  [{datatype, {enum, [true, false, peer, best_effort]}}]}. | ||||
| 
 | ||||
| {mapping, | ||||
|  "auth_oauth2.https.fail_if_no_peer_cert", | ||||
|  "rabbitmq_auth_backend_oauth2.key_config.fail_if_no_peer_cert", | ||||
|  [{datatype, {enum, [true, false]}}]}. | ||||
| 
 | ||||
| {validator, "https_uri", "According to the JWT Specification, Key Server URL must be https.", | ||||
|  fun(Uri) -> string:nth_lexeme(Uri, 1, "://") == "https" end}. | ||||
| 
 | ||||
| {mapping, | ||||
|  "auth_oauth2.algorithms.$algorithm", | ||||
|  "rabbitmq_auth_backend_oauth2.key_config.algorithms", | ||||
|  [{datatype, string}]}. | ||||
| 
 | ||||
| {translation, "rabbitmq_auth_backend_oauth2.key_config.algorithms", | ||||
|  fun(Conf) -> | ||||
|      Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.algorithms", Conf), | ||||
|      [list_to_binary(V) || {_, V} <- Settings] | ||||
|  end}. | ||||
|  |  | |||
|  | @ -0,0 +1,27 @@ | |||
| -module(uaa_jwks). | ||||
| -export([get/1]). | ||||
| 
 | ||||
| -spec get(string() | binary()) -> {ok, term()} | {error, term()}. | ||||
| get(JwksUrl) -> | ||||
|     httpc:request(get, {JwksUrl, []}, [{ssl, ssl_options()}, {timeout, 60000}], []). | ||||
| 
 | ||||
| -spec ssl_options() -> list(). | ||||
| ssl_options() -> | ||||
|     UaaEnv = application:get_env(rabbitmq_auth_backend_oauth2, key_config, []), | ||||
|     PeerVerification = proplists:get_value(peer_verification, UaaEnv, verify_none), | ||||
|     CaCertFile = proplists:get_value(cacertfile, UaaEnv), | ||||
|     Depth = proplists:get_value(depth, UaaEnv, 10), | ||||
|     FailIfNoPeerCert = proplists:get_value(fail_if_no_peer_cert, UaaEnv, false), | ||||
|     CrlCheck = proplists:get_value(crl_check, UaaEnv, false), | ||||
|     SslOpts0 = [{verify, PeerVerification}, | ||||
|                 {cacertfile, CaCertFile}, | ||||
|                 {depth, Depth}, | ||||
|                 {fail_if_no_peer_cert, FailIfNoPeerCert}, | ||||
|                 {crl_check, CrlCheck}, | ||||
|                 {crl_cache, {ssl_crl_cache, {internal, [{http, 10000}]}}}], | ||||
|     case proplists:get_value(hostname_verification, UaaEnv, none) of | ||||
|         wildcard -> | ||||
|             [{customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]} | SslOpts0]; | ||||
|         none -> | ||||
|             SslOpts0 | ||||
|     end. | ||||
|  | @ -58,7 +58,7 @@ update_jwks_signing_keys() -> | |||
|         undefined -> | ||||
|             {error, no_jwks_url}; | ||||
|         JwksUrl -> | ||||
|             case httpc:request(JwksUrl) of | ||||
|             case uaa_jwks:get(JwksUrl) of | ||||
|                 {ok, {_, _, JwksBody}} -> | ||||
|                     KeyList = maps:get(<<"keys">>, jose:decode(erlang:iolist_to_binary(JwksBody)), []), | ||||
|                     Keys = maps:from_list(lists:map(fun(Key) -> {maps:get(<<"kid">>, Key, undefined), {json, Key}} end, KeyList)), | ||||
|  |  | |||
|  | @ -24,7 +24,15 @@ decode(Token) -> | |||
|     end. | ||||
| 
 | ||||
| decode_and_verify(Jwk, Token) -> | ||||
|     case jose_jwt:verify(Jwk, Token) of | ||||
|     UaaEnv = application:get_env(rabbitmq_auth_backend_oauth2, key_config, []), | ||||
|     Verify = | ||||
|         case proplists:get_value(algorithms, UaaEnv) of | ||||
|             undefined -> | ||||
|                 jose_jwt:verify(Jwk, Token); | ||||
|             Algs -> | ||||
|                 jose_jwt:verify_strict(Jwk, Algs, Token) | ||||
|         end, | ||||
|     case Verify of | ||||
|         {true, #jose_jwt{fields = Fields}, _}  -> {true, Fields}; | ||||
|         {false, #jose_jwt{fields = Fields}, _} -> {false, Fields} | ||||
|     end. | ||||
|  |  | |||
|  | @ -4,7 +4,16 @@ | |||
|         auth_oauth2.additional_scopes_key = my_custom_scope_key | ||||
|         auth_oauth2.default_key = id1 | ||||
|         auth_oauth2.signing_keys.id1 = test/config_schema_SUITE_data/certs/key.pem | ||||
|         auth_oauth2.signing_keys.id2 = test/config_schema_SUITE_data/certs/cert.pem", | ||||
|         auth_oauth2.signing_keys.id2 = test/config_schema_SUITE_data/certs/cert.pem | ||||
|         auth_oauth2.jwks_url = https://my-jwt-issuer/jwks.json | ||||
|         auth_oauth2.https.cacertfile = test/config_schema_SUITE_data/certs/cacert.pem | ||||
|         auth_oauth2.https.peer_verification = verify_none | ||||
|         auth_oauth2.https.depth = 5 | ||||
|         auth_oauth2.https.fail_if_no_peer_cert = false | ||||
|         auth_oauth2.https.hostname_verification = wildcard | ||||
|         auth_oauth2.https.crl_check = true | ||||
|         auth_oauth2.algorithms.1 = HS256 | ||||
|         auth_oauth2.algorithms.2 = RS256", | ||||
|         [ | ||||
|         {rabbitmq_auth_backend_oauth2, [ | ||||
|             {resource_server_id,<<"new_resource_server_id">>}, | ||||
|  | @ -16,7 +25,15 @@ | |||
|                     <<"id1">> => {pem, <<"I'm not a certificate">>}, | ||||
|                     <<"id2">> => {pem, <<"I'm not a certificate">>} | ||||
|                 } | ||||
|             } | ||||
|             }, | ||||
|             {jwks_url, "https://my-jwt-issuer/jwks.json"}, | ||||
|             {cacertfile, "test/config_schema_SUITE_data/certs/cacert.pem"}, | ||||
|             {peer_verification, verify_none}, | ||||
|             {depth, 5}, | ||||
|             {fail_if_no_peer_cert, false}, | ||||
|             {hostname_verification, wildcard}, | ||||
|             {crl_check, true}, | ||||
|             {algorithms, [<<"HS256">>, <<"RS256">>]} | ||||
|             ] | ||||
|             } | ||||
|         ]} | ||||
|  |  | |||
|  | @ -21,7 +21,9 @@ | |||
| all() -> | ||||
|     [ | ||||
|      {group, happy_path}, | ||||
|      {group, unhappy_path} | ||||
|      {group, unhappy_path}, | ||||
|      {group, unvalidated_jwks_server}, | ||||
|      {group, no_peer_verification} | ||||
|     ]. | ||||
| 
 | ||||
| groups() -> | ||||
|  | @ -34,6 +36,7 @@ groups() -> | |||
|                        test_successful_connection_with_complex_claim_as_a_list, | ||||
|                        test_successful_connection_with_complex_claim_as_a_binary, | ||||
|                        test_successful_connection_with_keycloak_token, | ||||
|                        test_successful_connection_with_algorithm_restriction, | ||||
|                        test_successful_token_refresh | ||||
|                       ]}, | ||||
|      {unhappy_path, [], [ | ||||
|  | @ -41,9 +44,12 @@ groups() -> | |||
|                          test_failed_connection_with_a_non_token, | ||||
|                          test_failed_connection_with_a_token_with_insufficient_vhost_permission, | ||||
|                          test_failed_connection_with_a_token_with_insufficient_resource_permission, | ||||
|                          test_failed_connection_with_algorithm_restriction, | ||||
|                          test_failed_token_refresh_case1, | ||||
|                          test_failed_token_refresh_case2 | ||||
|                         ]} | ||||
|                         ]}, | ||||
|      {unvalidated_jwks_server, [], [test_failed_connection_with_unvalidated_jwks_server]}, | ||||
|      {no_peer_verification, [], [{group, happy_path}, {group, unhappy_path}]} | ||||
|     ]. | ||||
| 
 | ||||
| %% | ||||
|  | @ -69,23 +75,35 @@ end_per_suite(Config) -> | |||
|         fun stop_jwks_server/1 | ||||
|       ] ++ rabbit_ct_broker_helpers:teardown_steps()). | ||||
| 
 | ||||
| init_per_group(no_peer_verification, Config) -> | ||||
|     add_vhosts(Config), | ||||
|     KeyConfig = rabbit_ct_helpers:set_config(?config(key_config, Config), [{jwks_url, ?config(non_strict_jwks_url, Config)}, {peer_verification, verify_none}]), | ||||
|     ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_auth_backend_oauth2, key_config, KeyConfig]), | ||||
|     rabbit_ct_helpers:set_config(Config, {key_config, KeyConfig}); | ||||
| 
 | ||||
| init_per_group(_Group, Config) -> | ||||
|     %% The broker is managed by {init,end}_per_testcase(). | ||||
|     lists:foreach(fun(Value) -> | ||||
|                           rabbit_ct_broker_helpers:add_vhost(Config, Value) | ||||
|                   end, | ||||
|                   [<<"vhost1">>, <<"vhost2">>, <<"vhost3">>, <<"vhost4">>]), | ||||
|     add_vhosts(Config), | ||||
|     Config. | ||||
| 
 | ||||
| end_per_group(no_peer_verification, Config) -> | ||||
|     delete_vhosts(Config), | ||||
|     KeyConfig = rabbit_ct_helpers:set_config(?config(key_config, Config), [{jwks_url, ?config(strict_jwks_url, Config)}, {peer_verification, verify_peer}]), | ||||
|     ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_auth_backend_oauth2, key_config, KeyConfig]), | ||||
|     rabbit_ct_helpers:set_config(Config, {key_config, KeyConfig}); | ||||
| 
 | ||||
| end_per_group(_Group, Config) -> | ||||
|     %% The broker is managed by {init,end}_per_testcase(). | ||||
|     lists:foreach(fun(Value) -> | ||||
|                           rabbit_ct_broker_helpers:delete_vhost(Config, Value) | ||||
|                   end, | ||||
|                   [<<"vhost1">>, <<"vhost2">>, <<"vhost3">>, <<"vhost4">>]), | ||||
|     delete_vhosts(Config), | ||||
|     Config. | ||||
| 
 | ||||
| add_vhosts(Config) -> | ||||
|     %% The broker is managed by {init,end}_per_testcase(). | ||||
|     lists:foreach(fun(Value) -> rabbit_ct_broker_helpers:add_vhost(Config, Value) end, | ||||
|                   [<<"vhost1">>, <<"vhost2">>, <<"vhost3">>, <<"vhost4">>]). | ||||
| 
 | ||||
| delete_vhosts(Config) -> | ||||
|     %% The broker is managed by {init,end}_per_testcase(). | ||||
|     lists:foreach(fun(Value) -> rabbit_ct_broker_helpers:delete_vhost(Config, Value) end, | ||||
|                   [<<"vhost1">>, <<"vhost2">>, <<"vhost3">>, <<"vhost4">>]). | ||||
| 
 | ||||
| init_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_a_full_permission_token_and_explicitly_configured_vhost orelse | ||||
|                                          Testcase =:= test_successful_token_refresh -> | ||||
|  | @ -107,6 +125,24 @@ init_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection | |||
|   rabbit_ct_helpers:testcase_started(Config, Testcase), | ||||
|   Config; | ||||
| 
 | ||||
| init_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_algorithm_restriction -> | ||||
|     KeyConfig = ?config(key_config, Config), | ||||
|     ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_auth_backend_oauth2, key_config, [{algorithms, [<<"HS256">>]} | KeyConfig]]), | ||||
|     rabbit_ct_helpers:testcase_started(Config, Testcase), | ||||
|     Config; | ||||
| 
 | ||||
| init_per_testcase(Testcase, Config) when Testcase =:= test_failed_connection_with_algorithm_restriction -> | ||||
|     KeyConfig = ?config(key_config, Config), | ||||
|     ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_auth_backend_oauth2, key_config, [{algorithms, [<<"RS256">>]} | KeyConfig]]), | ||||
|     rabbit_ct_helpers:testcase_started(Config, Testcase), | ||||
|     Config; | ||||
| 
 | ||||
| init_per_testcase(Testcase, Config) when Testcase =:= test_failed_connection_with_unvalidated_jwks_server -> | ||||
|     KeyConfig = rabbit_ct_helpers:set_config(?config(key_config, Config), {jwks_url, ?config(non_strict_jwks_url, Config)}), | ||||
|     ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_auth_backend_oauth2, key_config, KeyConfig]), | ||||
|     rabbit_ct_helpers:testcase_started(Config, Testcase), | ||||
|     Config; | ||||
| 
 | ||||
| init_per_testcase(Testcase, Config) -> | ||||
|     rabbit_ct_helpers:testcase_started(Config, Testcase), | ||||
|     Config. | ||||
|  | @ -126,6 +162,14 @@ end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_ | |||
|   rabbit_ct_helpers:testcase_started(Config, Testcase), | ||||
|   Config; | ||||
| 
 | ||||
| end_per_testcase(Testcase, Config) when Testcase =:= test_successful_connection_with_algorithm_restriction orelse | ||||
|                                         Testcase =:= test_failed_connection_with_algorithm_restriction orelse | ||||
|                                         Testcase =:= test_failed_connection_with_unvalidated_jwks_server -> | ||||
|     rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), | ||||
|     ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_auth_backend_oauth2, key_config, ?config(key_config, Config)]), | ||||
|     rabbit_ct_helpers:testcase_finished(Config, Testcase), | ||||
|     Config; | ||||
| 
 | ||||
| end_per_testcase(Testcase, Config) -> | ||||
|     rabbit_ct_broker_helpers:delete_vhost(Config, <<"vhost1">>), | ||||
|     rabbit_ct_helpers:testcase_finished(Config, Testcase), | ||||
|  | @ -143,13 +187,27 @@ start_jwks_server(Config) -> | |||
|     %% Assume we don't have more than 100 ports allocated for tests | ||||
|     PortBase = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_ports_base), | ||||
|     JwksServerPort = PortBase + 100, | ||||
| 
 | ||||
|     %% Both URLs direct to the same JWKS server | ||||
|     %% The NonStrictJwksUrl identity cannot be validated while StrictJwksUrl identity can be validated | ||||
|     NonStrictJwksUrl = "https://127.0.0.1:" ++ integer_to_list(JwksServerPort) ++ "/jwks", | ||||
|     StrictJwksUrl = "https://localhost:" ++ integer_to_list(JwksServerPort) ++ "/jwks", | ||||
| 
 | ||||
|     ok = application:set_env(jwks_http, keys, [Jwk]), | ||||
|     {ok, _} = application:ensure_all_started(ssl), | ||||
|     {ok, _} = application:ensure_all_started(cowboy), | ||||
|     ok = jwks_http_app:start(JwksServerPort), | ||||
|     KeyConfig = [{jwks_url, "http://127.0.0.1:" ++ integer_to_list(JwksServerPort) ++ "/jwks"}], | ||||
|     CertsDir = ?config(rmq_certsdir, Config), | ||||
|     ok = jwks_http_app:start(JwksServerPort, CertsDir), | ||||
|     KeyConfig = [{jwks_url, StrictJwksUrl}, | ||||
|                  {peer_verification, verify_peer}, | ||||
|                  {cacertfile, filename:join([CertsDir, "testca", "cacert.pem"])}], | ||||
|     ok = rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, | ||||
|                                       [rabbitmq_auth_backend_oauth2, key_config, KeyConfig]), | ||||
|     rabbit_ct_helpers:set_config(Config, {fixture_jwk, Jwk}). | ||||
|     rabbit_ct_helpers:set_config(Config, | ||||
|                                  [{non_strict_jwks_url, NonStrictJwksUrl}, | ||||
|                                   {strict_jwks_url, StrictJwksUrl}, | ||||
|                                   {key_config, KeyConfig}, | ||||
|                                   {fixture_jwk, Jwk}]). | ||||
| 
 | ||||
| stop_jwks_server(Config) -> | ||||
|     ok = jwks_http_app:stop(), | ||||
|  | @ -305,7 +363,7 @@ test_successful_token_refresh(Config) -> | |||
|     Conn     = open_unmanaged_connection(Config, 0, <<"vhost1">>, <<"username">>, Token), | ||||
|     {ok, Ch} = amqp_connection:open_channel(Conn), | ||||
| 
 | ||||
|     {_Algo, Token2} = generate_valid_token(Config, [<<"rabbitmq.configure:vhost1/*">>, | ||||
|     {_Algo2, Token2} = generate_valid_token(Config, [<<"rabbitmq.configure:vhost1/*">>, | ||||
|                                                     <<"rabbitmq.write:vhost1/*">>, | ||||
|                                                     <<"rabbitmq.read:vhost1/*">>]), | ||||
|     ?UTIL_MOD:wait_for_token_to_expire(timer:seconds(Duration)), | ||||
|  | @ -321,6 +379,13 @@ test_successful_token_refresh(Config) -> | |||
|     amqp_channel:close(Ch2), | ||||
|     close_connection_and_channel(Conn, Ch). | ||||
| 
 | ||||
| test_successful_connection_with_algorithm_restriction(Config) -> | ||||
|     {_Algo, Token} = rabbit_ct_helpers:get_config(Config, fixture_jwt), | ||||
|     Conn = open_unmanaged_connection(Config, 0, <<"username">>, Token), | ||||
|     {ok, Ch} = amqp_connection:open_channel(Conn), | ||||
|     #'queue.declare_ok'{queue = _} = | ||||
|         amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), | ||||
|     close_connection_and_channel(Conn, Ch). | ||||
| 
 | ||||
| test_failed_connection_with_expired_token(Config) -> | ||||
|     {_Algo, Token} = generate_expired_token(Config, [<<"rabbitmq.configure:vhost1/*">>, | ||||
|  | @ -359,7 +424,7 @@ test_failed_token_refresh_case1(Config) -> | |||
|     #'queue.declare_ok'{queue = _} = | ||||
|         amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), | ||||
| 
 | ||||
|     {_Algo, Token2} = generate_expired_token(Config, [<<"rabbitmq.configure:vhost4/*">>, | ||||
|     {_Algo2, Token2} = generate_expired_token(Config, [<<"rabbitmq.configure:vhost4/*">>, | ||||
|                                                       <<"rabbitmq.write:vhost4/*">>, | ||||
|                                                       <<"rabbitmq.read:vhost4/*">>]), | ||||
|     %% the error is communicated asynchronously via a connection-level error | ||||
|  | @ -387,3 +452,13 @@ test_failed_token_refresh_case2(Config) -> | |||
|        amqp_connection:open_channel(Conn)), | ||||
| 
 | ||||
|     close_connection(Conn). | ||||
| 
 | ||||
| test_failed_connection_with_algorithm_restriction(Config) -> | ||||
|     {_Algo, Token} = rabbit_ct_helpers:get_config(Config, fixture_jwt), | ||||
|     ?assertMatch({error, {auth_failure, _}}, | ||||
|                  open_unmanaged_connection(Config, 0, <<"username">>, Token)). | ||||
| 
 | ||||
| test_failed_connection_with_unvalidated_jwks_server(Config) -> | ||||
|     {_Algo, Token} = rabbit_ct_helpers:get_config(Config, fixture_jwt), | ||||
|     ?assertMatch({error, {auth_failure, _}}, | ||||
|                  open_unmanaged_connection(Config, 0, <<"username">>, Token)). | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| -module(jwks_http_app). | ||||
| 
 | ||||
| -export([start/1, stop/0]). | ||||
| -export([start/2, stop/0]). | ||||
| 
 | ||||
| start(Port) -> | ||||
| start(Port, CertsDir) -> | ||||
|     Dispatch = | ||||
|         cowboy_router:compile( | ||||
|           [ | ||||
|  | @ -11,8 +11,10 @@ start(Port) -> | |||
|                  ]} | ||||
|           ] | ||||
|          ), | ||||
|     {ok, _} = cowboy:start_clear(jwks_http_listener, | ||||
|                       [{port, Port}], | ||||
|     {ok, _} = cowboy:start_tls(jwks_http_listener, | ||||
|                       [{port, Port}, | ||||
|                        {certfile, filename:join([CertsDir, "server", "cert.pem"])}, | ||||
|                        {keyfile, filename:join([CertsDir, "server", "key.pem"])}], | ||||
|                       #{env => #{dispatch => Dispatch}}), | ||||
|     ok. | ||||
| 
 | ||||
|  |  | |||
|  | @ -73,7 +73,7 @@ expired_token_with_scopes(Scopes) -> | |||
|     token_with_scopes_and_expiration(Scopes, os:system_time(seconds) - 10). | ||||
| 
 | ||||
| fixture_token_with_scopes(Scopes) -> | ||||
|     token_with_scopes_and_expiration(Scopes, os:system_time(seconds) + 10). | ||||
|     token_with_scopes_and_expiration(Scopes, os:system_time(seconds) + 30). | ||||
| 
 | ||||
| token_with_scopes_and_expiration(Scopes, Expiration) -> | ||||
|     %% expiration is a timestamp with precision in seconds | ||||
|  |  | |||
|  | @ -87,7 +87,9 @@ delete_super_stream(VirtualHost, Name, Username) -> | |||
|     gen_server:call(?MODULE, | ||||
|                     {delete_super_stream, VirtualHost, Name, Username}). | ||||
| 
 | ||||
| -spec lookup_leader(binary(), binary()) -> pid() | cluster_not_found. | ||||
| -spec lookup_leader(binary(), binary()) -> | ||||
|                        {ok, pid()} | {error, not_available} | | ||||
|                        {error, not_found}. | ||||
| lookup_leader(VirtualHost, Stream) -> | ||||
|     gen_server:call(?MODULE, {lookup_leader, VirtualHost, Stream}). | ||||
| 
 | ||||
|  | @ -294,20 +296,25 @@ handle_call({lookup_leader, VirtualHost, Stream}, _From, State) -> | |||
|                           LeaderPid = amqqueue:get_pid(Q), | ||||
|                           case process_alive(LeaderPid) of | ||||
|                               true -> | ||||
|                                   LeaderPid; | ||||
|                                   {ok, LeaderPid}; | ||||
|                               false -> | ||||
|                                   case leader_from_members(Q) of | ||||
|                                       {ok, Pid} -> | ||||
|                                           Pid; | ||||
|                                           {ok, Pid}; | ||||
|                                       _ -> | ||||
|                                           cluster_not_found | ||||
|                                           {error, not_available} | ||||
|                                   end | ||||
|                           end; | ||||
|                       _ -> | ||||
|                           cluster_not_found | ||||
|                           {error, not_found} | ||||
|                   end; | ||||
|               _ -> | ||||
|                   cluster_not_found | ||||
|               {error, not_found} -> | ||||
|                   case rabbit_amqqueue:not_found_or_absent_dirty(Name) of | ||||
|                       not_found -> | ||||
|                           {error, not_found}; | ||||
|                       _ -> | ||||
|                           {error, not_available} | ||||
|                   end | ||||
|           end, | ||||
|     {reply, Res, State}; | ||||
| handle_call({lookup_local_member, VirtualHost, Stream}, _From, | ||||
|  |  | |||
|  | @ -1494,7 +1494,7 @@ handle_frame_post_auth(Transport, | |||
|             of | ||||
|                 {false, false} -> | ||||
|                     case lookup_leader(Stream, Connection0) of | ||||
|                         cluster_not_found -> | ||||
|                         {error, not_found} -> | ||||
|                             response(Transport, | ||||
|                                      Connection0, | ||||
|                                      declare_publisher, | ||||
|  | @ -1504,6 +1504,16 @@ handle_frame_post_auth(Transport, | |||
|                                                                              ?STREAM_DOES_NOT_EXIST, | ||||
|                                                                              1), | ||||
|                             {Connection0, State}; | ||||
|                         {error, not_available} -> | ||||
|                             response(Transport, | ||||
|                                      Connection0, | ||||
|                                      declare_publisher, | ||||
|                                      CorrelationId, | ||||
|                                      ?RESPONSE_CODE_STREAM_NOT_AVAILABLE), | ||||
|                             rabbit_global_counters:increase_protocol_counter(stream, | ||||
|                                                                              ?STREAM_NOT_AVAILABLE, | ||||
|                                                                              1), | ||||
|                             {Connection0, State}; | ||||
|                         {ClusterLeader, | ||||
|                          #stream_connection{publishers = Publishers0, | ||||
|                                             publisher_to_ids = RefIds0} = | ||||
|  | @ -1960,9 +1970,9 @@ handle_frame_post_auth(_Transport, | |||
|     of | ||||
|         ok -> | ||||
|             case lookup_leader(Stream, Connection) of | ||||
|                 cluster_not_found -> | ||||
|                     rabbit_log:warning("Could not find leader to store offset on ~p", | ||||
|                                        [Stream]), | ||||
|                 {error, Error} -> | ||||
|                     rabbit_log:warning("Could not find leader to store offset on ~p: ~p", | ||||
|                                        [Stream, Error]), | ||||
|                     %% FIXME store offset is fire-and-forget, so no response even if error, change this? | ||||
|                     {Connection, State}; | ||||
|                 {ClusterLeader, Connection1} -> | ||||
|  | @ -1992,11 +2002,16 @@ handle_frame_post_auth(Transport, | |||
|         of | ||||
|             ok -> | ||||
|                 case lookup_leader(Stream, Connection0) of | ||||
|                     cluster_not_found -> | ||||
|                     {error, not_found} -> | ||||
|                         rabbit_global_counters:increase_protocol_counter(stream, | ||||
|                                                                          ?STREAM_DOES_NOT_EXIST, | ||||
|                                                                          1), | ||||
|                         {?RESPONSE_CODE_STREAM_DOES_NOT_EXIST, 0, Connection0}; | ||||
|                     {error, not_available} -> | ||||
|                         rabbit_global_counters:increase_protocol_counter(stream, | ||||
|                                                                          ?STREAM_NOT_AVAILABLE, | ||||
|                                                                          1), | ||||
|                         {?RESPONSE_CODE_STREAM_NOT_AVAILABLE, 0, Connection0}; | ||||
|                     {LeaderPid, C} -> | ||||
|                         {RC, O} = | ||||
|                             case osiris:read_tracking(LeaderPid, Reference) of | ||||
|  | @ -2532,9 +2547,9 @@ lookup_leader(Stream, | |||
|     case maps:get(Stream, StreamLeaders, undefined) of | ||||
|         undefined -> | ||||
|             case lookup_leader_from_manager(VirtualHost, Stream) of | ||||
|                 cluster_not_found -> | ||||
|                     cluster_not_found; | ||||
|                 LeaderPid -> | ||||
|                 {error, Error} -> | ||||
|                     {error, Error}; | ||||
|                 {ok, LeaderPid} -> | ||||
|                     Connection1 = | ||||
|                         maybe_monitor_stream(LeaderPid, Stream, Connection), | ||||
|                     {LeaderPid, | ||||
|  |  | |||
|  | @ -27,9 +27,9 @@ | |||
| 
 | ||||
|     <properties> | ||||
| 	<stream-client.version>[0.5.0-SNAPSHOT,1.0-SNAPSHOT)</stream-client.version> | ||||
|         <junit.jupiter.version>5.8.1</junit.jupiter.version> | ||||
|         <junit.jupiter.version>5.8.2</junit.jupiter.version> | ||||
|         <assertj.version>3.21.0</assertj.version> | ||||
|         <logback.version>1.2.6</logback.version> | ||||
|         <logback.version>1.2.7</logback.version> | ||||
|         <maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version> | ||||
|         <maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version> | ||||
|         <spotless.version>2.2.0</spotless.version> | ||||
|  |  | |||
|  | @ -16,6 +16,9 @@ | |||
| 
 | ||||
| package com.rabbitmq.stream; | ||||
| 
 | ||||
| import static com.rabbitmq.stream.TestUtils.ResponseConditions.ko; | ||||
| import static com.rabbitmq.stream.TestUtils.ResponseConditions.ok; | ||||
| import static com.rabbitmq.stream.TestUtils.ResponseConditions.responseCode; | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| 
 | ||||
| import com.rabbitmq.stream.impl.Client; | ||||
|  | @ -40,8 +43,7 @@ public class ClusterSizeTest { | |||
|     String s = UUID.randomUUID().toString(); | ||||
|     Response response = | ||||
|         client.create(s, Collections.singletonMap("initial-cluster-size", clusterSize)); | ||||
|     assertThat(response.isOk()).isFalse(); | ||||
|     assertThat(response.getResponseCode()).isEqualTo(Constants.RESPONSE_CODE_PRECONDITION_FAILED); | ||||
|     assertThat(response).is(ko()).has(responseCode(Constants.RESPONSE_CODE_PRECONDITION_FAILED)); | ||||
|   } | ||||
| 
 | ||||
|   @ParameterizedTest | ||||
|  | @ -53,7 +55,7 @@ public class ClusterSizeTest { | |||
|     try { | ||||
|       Response response = | ||||
|           client.create(s, Collections.singletonMap("initial-cluster-size", requestedClusterSize)); | ||||
|       assertThat(response.isOk()).isTrue(); | ||||
|       assertThat(response).is(ok()); | ||||
|       StreamMetadata metadata = client.metadata(s).get(s); | ||||
|       assertThat(metadata).isNotNull(); | ||||
|       assertThat(metadata.getResponseCode()).isEqualTo(Constants.RESPONSE_CODE_OK); | ||||
|  |  | |||
|  | @ -16,11 +16,16 @@ | |||
| 
 | ||||
| package com.rabbitmq.stream; | ||||
| 
 | ||||
| import static com.rabbitmq.stream.TestUtils.ResponseConditions.ok; | ||||
| import static com.rabbitmq.stream.TestUtils.waitAtMost; | ||||
| import static com.rabbitmq.stream.TestUtils.waitUntil; | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.assertj.core.api.Assertions.fail; | ||||
| 
 | ||||
| import com.rabbitmq.stream.codec.WrapperMessageBuilder; | ||||
| import com.rabbitmq.stream.impl.Client; | ||||
| import com.rabbitmq.stream.impl.Client.ClientParameters; | ||||
| import com.rabbitmq.stream.impl.Client.Response; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.time.Duration; | ||||
| import java.util.*; | ||||
|  | @ -66,7 +71,7 @@ public class FailureTest { | |||
|     Client.StreamMetadata streamMetadata = metadata.get(stream); | ||||
|     assertThat(streamMetadata).isNotNull(); | ||||
| 
 | ||||
|     TestUtils.waitUntil(() -> client.metadata(stream).get(stream).getReplicas().size() == 2); | ||||
|     waitUntil(() -> client.metadata(stream).get(stream).getReplicas().size() == 2); | ||||
| 
 | ||||
|     streamMetadata = client.metadata(stream).get(stream); | ||||
|     assertThat(streamMetadata.getLeader().getPort()).isEqualTo(TestUtils.streamPortNode1()); | ||||
|  | @ -107,7 +112,7 @@ public class FailureTest { | |||
|       assertThat(metadataLatch.await(10, TimeUnit.SECONDS)).isTrue(); | ||||
| 
 | ||||
|       // wait until there's a new leader | ||||
|       TestUtils.waitAtMost( | ||||
|       waitAtMost( | ||||
|           Duration.ofSeconds(10), | ||||
|           () -> { | ||||
|             Client.StreamMetadata m = publisher.metadata(stream).get(stream); | ||||
|  | @ -133,7 +138,7 @@ public class FailureTest { | |||
|     } | ||||
| 
 | ||||
|     // wait until all the replicas are there | ||||
|     TestUtils.waitAtMost( | ||||
|     waitAtMost( | ||||
|         Duration.ofSeconds(10), | ||||
|         () -> { | ||||
|           LOGGER.info("Getting metadata for {}", stream); | ||||
|  | @ -164,7 +169,7 @@ public class FailureTest { | |||
|                       consumeLatch.countDown(); | ||||
|                     })); | ||||
| 
 | ||||
|     TestUtils.waitAtMost( | ||||
|     waitAtMost( | ||||
|         Duration.ofSeconds(5), | ||||
|         () -> { | ||||
|           Client.Response response = | ||||
|  | @ -219,7 +224,7 @@ public class FailureTest { | |||
|                       cf.get(new Client.ClientParameters().port(TestUtils.streamPortNode2())); | ||||
|                   // wait until there's a new leader | ||||
|                   try { | ||||
|                     TestUtils.waitAtMost( | ||||
|                     waitAtMost( | ||||
|                         Duration.ofSeconds(5), | ||||
|                         () -> { | ||||
|                           Client.StreamMetadata m = locator.metadata(stream).get(stream); | ||||
|  | @ -314,7 +319,7 @@ public class FailureTest { | |||
| 
 | ||||
|     Client metadataClient = cf.get(new Client.ClientParameters().port(TestUtils.streamPortNode2())); | ||||
|     // wait until all the replicas are there | ||||
|     TestUtils.waitAtMost( | ||||
|     waitAtMost( | ||||
|         Duration.ofSeconds(5), | ||||
|         () -> { | ||||
|           Client.StreamMetadata m = metadataClient.metadata(stream).get(stream); | ||||
|  | @ -350,7 +355,7 @@ public class FailureTest { | |||
| 
 | ||||
|     Client.Response response = | ||||
|         consumer.subscribe((byte) 1, stream, OffsetSpecification.first(), 10); | ||||
|     assertThat(response.isOk()).isTrue(); | ||||
|     assertThat(response).is(ok()); | ||||
| 
 | ||||
|     assertThat(consumedLatch.await(5, TimeUnit.SECONDS)).isTrue(); | ||||
|     assertThat(generations).hasSize(2).contains(0L, 1L); | ||||
|  | @ -372,8 +377,7 @@ public class FailureTest { | |||
|     Client.StreamMetadata streamMetadata = metadata.get(stream); | ||||
|     assertThat(streamMetadata).isNotNull(); | ||||
| 
 | ||||
|     TestUtils.waitUntil( | ||||
|         () -> metadataClient.metadata(stream).get(stream).getReplicas().size() == 2); | ||||
|     waitUntil(() -> metadataClient.metadata(stream).get(stream).getReplicas().size() == 2); | ||||
| 
 | ||||
|     metadata = metadataClient.metadata(stream); | ||||
|     streamMetadata = metadata.get(stream); | ||||
|  | @ -497,7 +501,7 @@ public class FailureTest { | |||
| 
 | ||||
|     Client.Response response = | ||||
|         consumer.subscribe((byte) 1, stream, OffsetSpecification.first(), 10); | ||||
|     assertThat(response.isOk()).isTrue(); | ||||
|     assertThat(response).is(ok()); | ||||
| 
 | ||||
|     // let's publish for a bit of time | ||||
|     Thread.sleep(2000); | ||||
|  | @ -521,7 +525,7 @@ public class FailureTest { | |||
|     confirmedCount = confirmed.size(); | ||||
| 
 | ||||
|     // wait until all the replicas are there | ||||
|     TestUtils.waitAtMost( | ||||
|     waitAtMost( | ||||
|         Duration.ofSeconds(10), | ||||
|         () -> { | ||||
|           Client.StreamMetadata m = metadataClient.metadata(stream).get(stream); | ||||
|  | @ -535,9 +539,9 @@ public class FailureTest { | |||
| 
 | ||||
|     keepPublishing.set(false); | ||||
| 
 | ||||
|     assertThat(publishingLatch.await(5, TimeUnit.SECONDS)).isTrue(); | ||||
|     assertThat(publishingLatch.await(10, TimeUnit.SECONDS)).isTrue(); | ||||
| 
 | ||||
|     TestUtils.waitAtMost(Duration.ofSeconds(5), () -> consumed.size() >= confirmed.size()); | ||||
|     waitAtMost(Duration.ofSeconds(10), () -> consumed.size() >= confirmed.size()); | ||||
| 
 | ||||
|     assertThat(generations).hasSize(2).contains(0L, 1L); | ||||
|     assertThat(consumed).hasSizeGreaterThanOrEqualTo(confirmed.size()); | ||||
|  | @ -551,4 +555,33 @@ public class FailureTest { | |||
| 
 | ||||
|     confirmedIds.forEach(confirmedId -> assertThat(consumedIds).contains(confirmedId)); | ||||
|   } | ||||
| 
 | ||||
|   @Test | ||||
|   void declarePublisherShouldNotReturnStreamDoesNotExistOnRestart() throws Exception { | ||||
|     try { | ||||
|       Host.rabbitmqctl("stop_app"); | ||||
|     } finally { | ||||
|       Host.rabbitmqctl("start_app"); | ||||
|     } | ||||
|     AtomicReference<Client> client = new AtomicReference<>(); | ||||
|     waitUntil( | ||||
|         () -> { | ||||
|           try { | ||||
|             client.set(cf.get(new ClientParameters().port(TestUtils.streamPortNode1()))); | ||||
|           } catch (Exception e) { | ||||
| 
 | ||||
|           } | ||||
|           return client.get() != null; | ||||
|         }); | ||||
|     Set<Short> responseCodes = ConcurrentHashMap.newKeySet(); | ||||
| 
 | ||||
|     waitUntil( | ||||
|         () -> { | ||||
|           Response response = client.get().declarePublisher((byte) 0, null, stream); | ||||
|           responseCodes.add(response.getResponseCode()); | ||||
|           return response.isOk(); | ||||
|         }); | ||||
| 
 | ||||
|     assertThat(responseCodes).doesNotContain(Constants.RESPONSE_CODE_STREAM_DOES_NOT_EXIST); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -16,6 +16,9 @@ | |||
| 
 | ||||
| package com.rabbitmq.stream; | ||||
| 
 | ||||
| import static com.rabbitmq.stream.TestUtils.ResponseConditions.ko; | ||||
| import static com.rabbitmq.stream.TestUtils.ResponseConditions.ok; | ||||
| import static com.rabbitmq.stream.TestUtils.ResponseConditions.responseCode; | ||||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| 
 | ||||
|  | @ -47,8 +50,7 @@ public class LeaderLocatorTest { | |||
|     Client client = cf.get(new Client.ClientParameters().port(TestUtils.streamPortNode1())); | ||||
|     String s = UUID.randomUUID().toString(); | ||||
|     Response response = client.create(s, Collections.singletonMap("queue-leader-locator", "foo")); | ||||
|     assertThat(response.isOk()).isFalse(); | ||||
|     assertThat(response.getResponseCode()).isEqualTo(Constants.RESPONSE_CODE_PRECONDITION_FAILED); | ||||
|     assertThat(response).is(ko()).has(responseCode(Constants.RESPONSE_CODE_PRECONDITION_FAILED)); | ||||
|   } | ||||
| 
 | ||||
|   @Test | ||||
|  | @ -60,7 +62,7 @@ public class LeaderLocatorTest { | |||
|       try { | ||||
|         Response response = | ||||
|             client.create(s, Collections.singletonMap("queue-leader-locator", "client-local")); | ||||
|         assertThat(response.isOk()).isTrue(); | ||||
|         assertThat(response).is(ok()); | ||||
|         StreamMetadata metadata = client.metadata(s).get(s); | ||||
|         assertThat(metadata).isNotNull(); | ||||
|         assertThat(metadata.getResponseCode()).isEqualTo(Constants.RESPONSE_CODE_OK); | ||||
|  | @ -136,7 +138,7 @@ public class LeaderLocatorTest { | |||
|                 Response response = | ||||
|                     client.create( | ||||
|                         s, Collections.singletonMap("queue-leader-locator", "least-leaders")); | ||||
|                 assertThat(response.isOk()).isTrue(); | ||||
|                 assertThat(response).is(ok()); | ||||
|                 createdStreams.add(s); | ||||
|               }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,11 +16,13 @@ | |||
| 
 | ||||
| package com.rabbitmq.stream; | ||||
| 
 | ||||
| import static com.rabbitmq.stream.TestUtils.ResponseConditions.ok; | ||||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.junit.jupiter.api.Assertions.fail; | ||||
| 
 | ||||
| import com.rabbitmq.stream.impl.Client; | ||||
| import com.rabbitmq.stream.impl.Client.Response; | ||||
| import io.netty.channel.EventLoopGroup; | ||||
| import io.netty.channel.nio.NioEventLoopGroup; | ||||
| import java.lang.reflect.Field; | ||||
|  | @ -30,6 +32,7 @@ import java.util.Set; | |||
| import java.util.UUID; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.function.BooleanSupplier; | ||||
| import org.assertj.core.api.Condition; | ||||
| import org.junit.jupiter.api.TestInfo; | ||||
| import org.junit.jupiter.api.extension.*; | ||||
| 
 | ||||
|  | @ -106,7 +109,7 @@ public class TestUtils { | |||
|                     .eventLoopGroup(eventLoopGroup(context)) | ||||
|                     .port(streamPortNode1())); | ||||
|         Client.Response response = client.create(stream); | ||||
|         assertThat(response.isOk()).isTrue(); | ||||
|         assertThat(response).is(ok()); | ||||
|         client.close(); | ||||
|         store(context).put("testMethodStream", stream); | ||||
|       } catch (NoSuchFieldException e) { | ||||
|  | @ -136,7 +139,7 @@ public class TestUtils { | |||
|                     .eventLoopGroup(eventLoopGroup(context)) | ||||
|                     .port(streamPortNode1())); | ||||
|         Client.Response response = client.delete(stream); | ||||
|         assertThat(response.isOk()).isTrue(); | ||||
|         assertThat(response).is(ok()); | ||||
|         client.close(); | ||||
|         store(context).remove("testMethodStream"); | ||||
|       } catch (NoSuchFieldException e) { | ||||
|  | @ -197,4 +200,22 @@ public class TestUtils { | |||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   static class ResponseConditions { | ||||
| 
 | ||||
|     static Condition<Response> ok() { | ||||
|       return new Condition<>(Response::isOk, "Response should be OK"); | ||||
|     } | ||||
| 
 | ||||
|     static Condition<Response> ko() { | ||||
|       return new Condition<>(response -> !response.isOk(), "Response should be OK"); | ||||
|     } | ||||
| 
 | ||||
|     static Condition<Response> responseCode(short expectedResponse) { | ||||
|       return new Condition<>( | ||||
|           response -> response.getResponseCode() == expectedResponse, | ||||
|           "response code %d", | ||||
|           expectedResponse); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ all() -> | |||
|     [{group, non_parallel_tests}]. | ||||
| 
 | ||||
| groups() -> | ||||
|     [{non_parallel_tests, [], [manage_super_stream]}]. | ||||
|     [{non_parallel_tests, [], [manage_super_stream, lookup_leader]}]. | ||||
| 
 | ||||
| %% ------------------------------------------------------------------- | ||||
| %% Testsuite setup/teardown. | ||||
|  | @ -71,6 +71,17 @@ end_per_testcase(Testcase, Config) -> | |||
| %% Testcases. | ||||
| %% ------------------------------------------------------------------- | ||||
| 
 | ||||
| lookup_leader(Config) -> | ||||
|     Stream = <<"stream_manager_lookup_leader_stream">>, | ||||
|     ?assertMatch({ok, _}, create_stream(Config, Stream)), | ||||
| 
 | ||||
|     {ok, Pid} = lookup_leader(Config, Stream), | ||||
|     ?assert(is_pid(Pid)), | ||||
| 
 | ||||
|     ?assertEqual({error, not_found}, lookup_leader(Config, <<"foo">>)), | ||||
| 
 | ||||
|     ?assertEqual({ok, deleted}, delete_stream(Config, Stream)). | ||||
| 
 | ||||
| manage_super_stream(Config) -> | ||||
|     % create super stream | ||||
|     ?assertEqual(ok, | ||||
|  | @ -140,6 +151,20 @@ create_stream(Config, Name) -> | |||
|                                  create, | ||||
|                                  [<<"/">>, Name, [], <<"guest">>]). | ||||
| 
 | ||||
| delete_stream(Config, Name) -> | ||||
|     rabbit_ct_broker_helpers:rpc(Config, | ||||
|                                  0, | ||||
|                                  rabbit_stream_manager, | ||||
|                                  delete, | ||||
|                                  [<<"/">>, Name, <<"guest">>]). | ||||
| 
 | ||||
| lookup_leader(Config, Name) -> | ||||
|     rabbit_ct_broker_helpers:rpc(Config, | ||||
|                                  0, | ||||
|                                  rabbit_stream_manager, | ||||
|                                  lookup_leader, | ||||
|                                  [<<"/">>, Name]). | ||||
| 
 | ||||
| partitions(Config, Name) -> | ||||
|     rabbit_ct_broker_helpers:rpc(Config, | ||||
|                                  0, | ||||
|  |  | |||
|  | @ -27,11 +27,11 @@ | |||
| 
 | ||||
|     <properties> | ||||
| 	<stream-client.version>[0.5.0-SNAPSHOT,1.0-SNAPSHOT)</stream-client.version> | ||||
|         <junit.jupiter.version>5.8.1</junit.jupiter.version> | ||||
|         <junit.jupiter.version>5.8.2</junit.jupiter.version> | ||||
|         <assertj.version>3.21.0</assertj.version> | ||||
|         <okhttp.version>4.9.2</okhttp.version> | ||||
|         <okhttp.version>4.9.3</okhttp.version> | ||||
|         <gson.version>2.8.9</gson.version> | ||||
|         <logback.version>1.2.6</logback.version> | ||||
|         <logback.version>1.2.7</logback.version> | ||||
|         <maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version> | ||||
|         <maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version> | ||||
|         <spotless.version>2.2.0</spotless.version> | ||||
|  |  | |||
|  | @ -15,7 +15,9 @@ deps_dirs: | |||
|   - bazel-bin/external/* | ||||
| include_dirs: | ||||
|   - deps | ||||
|   - deps/* | ||||
|   - deps/*/include | ||||
|   - deps/*/src | ||||
|   - bazel-bin/external | ||||
|   - bazel-bin/external/*/include | ||||
| plt_path: bazel-bin/deps/rabbit/.base_plt.plt | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue