Merge pull request #1130 from DeterminateSystems/prompt-password

hydra-create-user: support prompting for password
This commit is contained in:
Graham Christensen 2022-01-21 15:38:39 -05:00 committed by GitHub
commit 44cd890ae3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 112 additions and 21 deletions

View file

@ -28,7 +28,7 @@ Once the Hydra service has been configured as above and activate you should alre
```
$ su - hydra
$ hydra-create-user <USER> --full-name '<NAME>' \
--email-address '<EMAIL>' --password <PASSWORD> --role admin
--email-address '<EMAIL>' --password-prompt --role admin
```
Afterwards you should be able to log by clicking on "_Sign In_" on the top right of the web interface using the credentials specified by `hydra-create-user`. Once you are logged in you can click "_Admin -> Create Project_" to configure your first project.

View file

@ -13,7 +13,7 @@
* Creating a user:
$ hydra-create-user root --email-address 'e.dolstra@tudelft.nl' \
--password-hash "$(echo -n foobar | sha1sum | cut -c1-40)"
--password-prompt
(Replace "foobar" with the desired password.)

View file

@ -114,7 +114,7 @@ This can be done using the command `hydra-create-user`:
```console
$ hydra-create-user alice --full-name 'Alice Q. User' \
--email-address 'alice@example.org' --password foobar --role admin
--email-address 'alice@example.org' --password-prompt --role admin
```
Additional users can be created through the web interface.

View file

@ -495,6 +495,7 @@
StringCompareConstantTime
SysHostnameLong
TermSizeAny
TermReadKey
Test2Harness
TestMore
TestPostgreSQL

View file

@ -16,20 +16,32 @@ use IPC::Run;
our @ISA = qw(Exporter);
our @EXPORT = qw(
getHydraHome getHydraConfig getBaseUrl
getSCMCacheDir getStatsdConfig
registerRoot getGCRootsDir gcRootFor
jobsetOverview jobsetOverview_
getDrvLogPath findLog
getMainOutput
cancelBuilds
captureStdoutStderr
captureStdoutStderrWithStdin
findLog
gcRootFor
getBaseUrl
getDrvLogPath
getEvals getMachines
pathIsInsidePrefix
captureStdoutStderr run grab
getTotalShares
getGCRootsDir
getHydraConfig
getHydraHome
getMainOutput
getSCMCacheDir
getStatsdConfig
getStoreUri
readNixFile
getTotalShares
grab
isLocalStore
cancelBuilds restartBuilds);
jobsetOverview
jobsetOverview_
pathIsInsidePrefix
readNixFile
registerRoot
restartBuilds
run
);
sub getHydraHome {
@ -417,14 +429,19 @@ sub pathIsInsidePrefix {
sub captureStdoutStderr {
my ($timeout, @cmd) = @_;
my $stdin = "";
return captureStdoutStderrWithStdin($timeout, \@cmd, "");
}
sub captureStdoutStderrWithStdin {
my ($timeout, $cmd, $stdin) = @_;
my $stdout;
my $stderr;
eval {
local $SIG{ALRM} = sub { die "timeout\n" }; # NB: \n required
alarm $timeout;
IPC::Run::run(\@cmd, \$stdin, \$stdout, \$stderr);
IPC::Run::run($cmd, \$stdin, \$stdout, \$stderr);
alarm 0;
1;
} or do {

View file

@ -5,6 +5,7 @@ use warnings;
use Hydra::Schema;
use Hydra::Helper::Nix;
use Hydra::Model::DB;
use Term::ReadKey;
use Getopt::Long qw(:config gnu_getopt);
sub showHelp {
@ -14,6 +15,7 @@ Usage: hydra-create-user NAME
[--type hydra|google|github]
[--full-name FULLNAME]
[--email-address EMAIL-ADDRESS]
[--password-prompt]
[--password-hash HASH]
[--password PASSWORD (dangerous)]
[--wipe-roles]
@ -27,6 +29,16 @@ renamed.
* Specifying Passwords
** Interactively
Pass `--password-prompt` to collect the password on stdin.
The password will be hashed with Argon2id when stored.
Example:
$ hydra-create-user alice --password-prompt --role admin
** Specifying a Hash
You can generate a password hash and provide the hash as well. This
@ -63,7 +75,7 @@ Example:
exit 0;
}
my ($renameFrom, $type, $fullName, $emailAddress, $password, $passwordHash);
my ($renameFrom, $type, $fullName, $emailAddress, $password, $passwordHash, $passwordPrompt);
my $wipeRoles = 0;
my @roles;
@ -72,6 +84,7 @@ GetOptions("rename-from=s" => \$renameFrom,
"full-name=s" => \$fullName,
"email-address=s" => \$emailAddress,
"password=s" => \$password,
"password-prompt" => \$passwordPrompt,
"password-hash=s" => \$passwordHash,
"wipe-roles" => \$wipeRoles,
"role=s" => \@roles,
@ -81,9 +94,9 @@ GetOptions("rename-from=s" => \$renameFrom,
die "$0: one user name required\n" if scalar @ARGV != 1;
my $userName = $ARGV[0];
my $chosenPasswordOptions = grep { defined($_) } ($passwordHash, $password);
my $chosenPasswordOptions = grep { defined($_) } ($passwordPrompt, $passwordHash, $password);
if ($chosenPasswordOptions > 1) {
die "$0: please specify only one --password* option. See --help for more information.\n";
die "$0: please specify only one of --password-prompt or --password-hash. See --help for more information.\n";
}
die "$0: type must be `hydra', `google' or `github'\n"
@ -118,17 +131,43 @@ $db->txn_do(sub {
if defined $password;
die "$0: Google and GitHub accounts do not have a password.\n"
if defined $passwordHash;
die "$0: Google and GitHub accounts do not have a password.\n"
if defined $passwordPrompt;
$user->update({ emailaddress => $userName, password => "!" });
} else {
$user->update({ emailaddress => $emailAddress }) if defined $emailAddress;
if (defined $password && !(defined $passwordHash)) {
if (defined $password) {
# !!! TODO: Remove support for plaintext passwords in 2023.
print STDERR "Submitting plaintext passwords as arguments is deprecated and will be removed. See --help for alternatives.\n";
$user->setPassword($password);
}
if (defined $passwordHash) {
$user->setPasswordHash($passwordHash);
}
if (defined $passwordPrompt) {
ReadMode 2;
print STDERR "Password: ";
my $password = <STDIN> // "";
chomp $password;
print STDERR "\nPassword Confirmation: ";
my $passwordConfirm = <STDIN> // "";
chomp $passwordConfirm;
ReadMode 0;
print STDERR "\n";
if ($password ne $passwordConfirm) {
die "Passwords don't match."
} elsif ($password eq "") {
die "Password cannot be empty."
}
$user->setPassword($password);
}
}
$user->userroles->delete if $wipeRoles;

View file

@ -5,6 +5,7 @@ package CliRunners;
our @ISA = qw(Exporter);
our @EXPORT = qw(
captureStdoutStderr
captureStdoutStderrWithStdin
evalFails
evalSucceeds
runBuild
@ -21,6 +22,15 @@ sub captureStdoutStderr {
return Hydra::Helper::Nix::captureStdoutStderr(@_)
}
sub captureStdoutStderrWithStdin {
# "Lazy"-load Hydra::Helper::Nix to avoid the compile-time
# import of Hydra::Model::DB. Early loading of the DB class
# causes fixation of the DSN, and we need to fixate it after
# the temporary DB is setup.
require Hydra::Helper::Nix;
return Hydra::Helper::Nix::captureStdoutStderrWithStdin(@_)
}
sub evalSucceeds {
my ($jobset) = @_;
my ($res, $stdout, $stderr) = captureStdoutStderr(60, ("hydra-eval-jobset", $jobset->project->name, $jobset->name));

View file

@ -13,6 +13,7 @@ use CliRunners;
our @ISA = qw(Exporter);
our @EXPORT = qw(
captureStdoutStderr
captureStdoutStderrWithStdin
createBaseJobset
createJobsetWithOneInput
evalFails

View file

@ -10,6 +10,7 @@ subtest "Handling password and password hash creation" => sub {
subtest "Creating a user with a plain text password (insecure) stores the password securely" => sub {
my ($res, $stdout, $stderr) = captureStdoutStderr(5, ("hydra-create-user", "plain-text-user", "--password", "foobar"));
is($res, 0, "hydra-create-user should exit zero");
like($stderr, qr/Submitting plaintext passwords as arguments is deprecated and will be removed/, "Submitting a plain text password is deprecated.");
my $user = $db->resultset('Users')->find({ username => "plain-text-user" });
isnt($user, undef, "The user exists");
@ -46,15 +47,37 @@ subtest "Handling password and password hash creation" => sub {
is($storedPassword, $user->password, "The password was not upgraded.");
};
subtest "Creating a user by prompting for the password" => sub {
subtest "with the same password twice" => sub {
my ($res, $stdout, $stderr) = captureStdoutStderrWithStdin(5, ["hydra-create-user", "prompted-pass-user", "--password-prompt"], "my-password\nmy-password\n");
is($res, 0, "hydra-create-user should exit zero");
my $user = $db->resultset('Users')->find({ username => "prompted-pass-user" });
isnt($user, undef, "The user exists");
like($user->password, qr/^\$argon2id\$v=/, "The password was saved, hashed with argon2id.");
my $storedPassword = $user->password;
ok($user->check_password("my-password"), "Their password validates");
};
subtest "With mismatched password confirmation" => sub {
my ($res, $stdout, $stderr) = captureStdoutStderrWithStdin(5, ["hydra-create-user", "prompted-pass-user", "--password-prompt"], "my-password\nnot-my-password\n");
isnt($res, 0, "hydra-create-user should exit non-zero");
};
};
subtest "Specifying conflicting password options fails" => sub {
my @cases = (
[ "--password=foo", "--password-hash=8843d7f92416211de9ebb963ff4ce28125932878", "--password-prompt" ],
[ "--password=foo", "--password-prompt" ],
[ "--password=foo", "--password-hash=8843d7f92416211de9ebb963ff4ce28125932878" ],
[ "--password-hash=8843d7f92416211de9ebb963ff4ce28125932878", "--password-prompt" ],
);
for my $case (@cases) {
my ($res, $stdout, $stderr) = captureStdoutStderr(5, (
"hydra-create-user", "bogus-password-options", @{$case}));
like($stderr, qr/please specify only one --password\* option/, "We get an error about specifying the password");
like($stderr, qr/please specify only one of --password-prompt or --password-hash/, "We get an error about specifying the password");
isnt($res, 0, "hydra-create-user should exit non-zero with conflicting " . join(" ", @{$case}));
}
};