diff --git a/README.md b/README.md index 50e7ba6d..54cb9a93 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Once the Hydra service has been configured as above and activate you should alre ``` $ su - hydra $ hydra-create-user --full-name '' \ - --email-address '' --password --role admin + --email-address '' --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. diff --git a/doc/dev-notes.txt b/doc/dev-notes.txt index caadb57d..4035c809 100644 --- a/doc/dev-notes.txt +++ b/doc/dev-notes.txt @@ -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.) diff --git a/doc/manual/src/installation.md b/doc/manual/src/installation.md index 626d8286..cbf3f907 100644 --- a/doc/manual/src/installation.md +++ b/doc/manual/src/installation.md @@ -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. diff --git a/flake.nix b/flake.nix index 1a8325d3..3e382d9f 100644 --- a/flake.nix +++ b/flake.nix @@ -495,6 +495,7 @@ StringCompareConstantTime SysHostnameLong TermSizeAny + TermReadKey Test2Harness TestMore TestPostgreSQL diff --git a/src/lib/Hydra/Helper/Nix.pm b/src/lib/Hydra/Helper/Nix.pm index 7113cb4a..55c1f6f3 100644 --- a/src/lib/Hydra/Helper/Nix.pm +++ b/src/lib/Hydra/Helper/Nix.pm @@ -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 { diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user index ec14de95..cf9e8316 100755 --- a/src/script/hydra-create-user +++ b/src/script/hydra-create-user @@ -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 = // ""; + chomp $password; + + print STDERR "\nPassword Confirmation: "; + my $passwordConfirm = // ""; + 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; diff --git a/t/lib/CliRunners.pm b/t/lib/CliRunners.pm index e693eeb7..ddaa34ae 100644 --- a/t/lib/CliRunners.pm +++ b/t/lib/CliRunners.pm @@ -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)); diff --git a/t/lib/Setup.pm b/t/lib/Setup.pm index d7772731..56e01bdc 100644 --- a/t/lib/Setup.pm +++ b/t/lib/Setup.pm @@ -13,6 +13,7 @@ use CliRunners; our @ISA = qw(Exporter); our @EXPORT = qw( captureStdoutStderr + captureStdoutStderrWithStdin createBaseJobset createJobsetWithOneInput evalFails diff --git a/t/scripts/hydra-create-user.t b/t/scripts/hydra-create-user.t index 597f4ebc..66d019ef 100644 --- a/t/scripts/hydra-create-user.t +++ b/t/scripts/hydra-create-user.t @@ -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})); } };