Merge pull request #9669 from rabbitmq/mk-rabbitmqctl-accept-password-hashes
CLI: support for pre-hashed passwords
This commit is contained in:
commit
5208e7d74e
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue