Merge pull request #9669 from rabbitmq/mk-rabbitmqctl-accept-password-hashes

CLI: support for pre-hashed passwords
This commit is contained in:
Michael Klishin 2023-10-10 11:55:11 -04:00 committed by GitHub
commit 5208e7d74e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 117 additions and 13 deletions

View File

@ -22,7 +22,9 @@
add_user_sans_validation/3, put_user/2, put_user/3,
update_user/5,
update_user_with_hash/5,
add_user_sans_validation/6]).
add_user_sans_validation/6,
add_user_with_pre_hashed_password_sans_validation/3
]).
-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]).
@ -222,6 +224,10 @@ add_user(Username, Password, ActingUser, Limits, Tags) ->
validate_and_alternate_credentials(Username, Password, ActingUser,
add_user_sans_validation(Limits, Tags)).
add_user_with_pre_hashed_password_sans_validation(Username, PasswordHash, ActingUser) ->
HashingAlgorithm = rabbit_password:hashing_mod(),
add_user_sans_validation(Username, PasswordHash, HashingAlgorithm, [], undefined, ActingUser).
add_user_sans_validation(Username, Password, ActingUser) ->
add_user_sans_validation(Username, Password, ActingUser, undefined, []).
@ -246,14 +252,12 @@ add_user_sans_validation(Username, Password, ActingUser, Limits, Tags) ->
end,
add_user_sans_validation_in(Username, User, ConvertedTags, Limits, ActingUser).
add_user_sans_validation(Username, PasswordHash, HashingAlgorithm, Tags, Limits, ActingUser) ->
add_user_sans_validation(Username, PasswordHash, HashingMod, Tags, Limits, ActingUser) ->
rabbit_log:debug("Asked to create a new user '~ts' 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),
internal_user:set_password_hash(User0, PasswordHash, HashingMod),
ConvertedTags),
User = case Limits of
undefined -> User1;

View File

@ -54,6 +54,7 @@ defmodule RabbitMQ.CLI.Core.DocGuide do
Macros.defguide("monitoring")
Macros.defguide("networking")
Macros.defguide("parameters")
Macros.defguide("passwords")
Macros.defguide("plugins")
Macros.defguide("prometheus")
Macros.defguide("publishers")

View File

@ -10,21 +10,37 @@ defmodule RabbitMQ.CLI.Ctl.Commands.AddUserCommand do
@behaviour RabbitMQ.CLI.CommandBehaviour
use RabbitMQ.CLI.Core.MergesNoDefaults
def switches(), do: [pre_hashed_password: :boolean]
def merge_defaults(args, opts) do
{args, Map.merge(%{pre_hashed_password: false}, opts)}
end
def validate(args, _) when length(args) < 1, do: {:validation_failure, :not_enough_args}
def validate(args, _) when length(args) > 2, do: {:validation_failure, :too_many_args}
def validate([_], _), do: :ok
# Password will be provided via standard input
def validate([_username], _), do: :ok
def validate(["", _], _) do
{:validation_failure, {:bad_argument, "user cannot be an empty string"}}
end
def validate([_, base64_encoded_password_hash], %{pre_hashed_password: true}) do
case Base.decode64(base64_encoded_password_hash) do
{:ok, _password_hash} ->
:ok
_ ->
{:validation_failure,
{:bad_argument, "Could not Base64 decode provided password hash value"}}
end
end
def validate([_, _], _), do: :ok
use RabbitMQ.CLI.Core.RequiresRabbitAppRunning
def run([username], %{node: node_name} = opts) do
def run([username], %{node: node_name, pre_hashed_password: false} = opts) do
# note: blank passwords are currently allowed, they make sense
# e.g. when a user only authenticates using X.509 certificates.
# Credential validators can be used to require passwords of a certain length
@ -43,6 +59,46 @@ defmodule RabbitMQ.CLI.Ctl.Commands.AddUserCommand do
end
end
def run([username], %{node: node_name, pre_hashed_password: true} = opts) do
case Input.infer_password("Hashed and salted password: ", opts) do
:eof ->
{:error, :not_enough_args}
base64_encoded_password_hash ->
case Base.decode64(base64_encoded_password_hash) do
{:ok, password_hash} ->
:rabbit_misc.rpc_call(
node_name,
:rabbit_auth_backend_internal,
:add_user_with_pre_hashed_password_sans_validation,
[username, password_hash, Helpers.cli_acting_user()]
)
_ ->
{:error, ExitCodes.exit_dataerr(),
"Could not Base64 decode provided password hash value"}
end
end
end
def run(
[username, base64_encoded_password_hash],
%{node: node_name, pre_hashed_password: true} = opts
) do
case Base.decode64(base64_encoded_password_hash) do
{:ok, password_hash} ->
:rabbit_misc.rpc_call(
node_name,
:rabbit_auth_backend_internal,
:add_user_with_pre_hashed_password_sans_validation,
[username, password_hash, Helpers.cli_acting_user()]
)
_ ->
{:error, ExitCodes.exit_dataerr(), "Could not Base64 decode provided password hash value"}
end
end
def run([username, password], %{node: node_name}) do
:rabbit_misc.rpc_call(
node_name,
@ -89,21 +145,30 @@ defmodule RabbitMQ.CLI.Ctl.Commands.AddUserCommand do
use RabbitMQ.CLI.DefaultOutput
def usage, do: "add_user <username> <password>"
def usage, do: "add_user <username> [<password>] [<password_hash> --pre-hashed-password]"
def usage_additional() do
[
["<username>", "Self-explanatory"],
[
"<password>",
"Password this user will authenticate with. Use a blank string to disable password-based authentication."
"Password this user will authenticate with. Use a blank string to disable password-based authentication. Mutually exclusive with <password_hash>"
],
[
"<password_hash>",
"A Base64-encoded password hash produced by the 'hash_password' command or a different method as described in the Passwords guide. Must be used in combination with --pre-hashed-password. Mutually exclusive with <password>"
],
[
"--pre-hashed-password",
"Use to pass in a password hash instead of a clear text password. Disabled by default"
]
]
end
def usage_doc_guides() do
[
DocGuide.access_control()
DocGuide.access_control(),
DocGuide.passwords()
]
end

View File

@ -9,6 +9,8 @@ defmodule AddUserCommandTest do
import TestHelper
@command RabbitMQ.CLI.Ctl.Commands.AddUserCommand
@hash_password_command RabbitMQ.CLI.Ctl.Commands.HashPasswordCommand
@authenticate_user_command RabbitMQ.CLI.Ctl.Commands.AuthenticateUserCommand
setup_all do
RabbitMQ.CLI.Core.Distribution.start()
@ -18,7 +20,7 @@ defmodule AddUserCommandTest do
setup context do
on_exit(context, fn -> delete_user(context[:user]) end)
{:ok, opts: %{node: get_rabbit_hostname()}}
{:ok, opts: %{node: get_rabbit_hostname(), pre_hashed_password: false}}
end
test "validate: no positional arguments fails" do
@ -55,6 +57,17 @@ defmodule AddUserCommandTest do
assert @command.validate([context[:user], context[:password]], context[:opts]) == :ok
end
@tag user: "someone"
test "validate: pre-hashed with a non-Base64-encoded value returns an error", context do
hashed = "this is not a Base64-encoded value"
opts = Map.merge(context[:opts], %{pre_hashed_password: true})
assert match?(
{:validation_failure, {:bad_argument, _}},
@command.validate([context[:user], hashed], opts)
)
end
@tag user: "someone", password: "password"
test "run: request to a non-existent node returns a badrpc", context do
opts = %{node: :jake@thedog, timeout: 200}
@ -62,9 +75,30 @@ defmodule AddUserCommandTest do
end
@tag user: "someone", password: "password"
test "run: default case completes successfully", context do
test "run: happy path completes successfully", context do
assert @command.run([context[:user], context[:password]], context[:opts]) == :ok
assert list_users() |> Enum.count(fn record -> record[:user] == context[:user] end) == 1
assert @authenticate_user_command.run([context[:user], context[:password]], context[:opts])
end
@tag user: "someone"
test "run: a pre-hashed request to a non-existent node returns a badrpc", context do
opts = %{node: :jake@thedog, timeout: 200}
hashed = "BMT6cj/MsI+4UOBtsPPQWpQfk7ViRLj4VqpMTxu54FU3qa1G"
assert match?({:badrpc, _}, @command.run([context[:user], hashed], opts))
end
@tag user: "someone"
test "run: pre-hashed happy path completes successfully", context do
pwd = "guest10"
hashed = @hash_password_command.hash_password(pwd)
opts = Map.merge(%{pre_hashed_password: true}, context[:opts])
assert @command.run([context[:user], hashed], opts) == :ok
assert list_users() |> Enum.count(fn record -> record[:user] == context[:user] end) == 1
assert @authenticate_user_command.run([context[:user], pwd], opts)
end
@tag user: "someone", password: "password"