From d4fe7e55dd6d5d84a41a2d7747756a59543f5323 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Fri, 21 Jan 2022 09:22:12 -0500 Subject: [PATCH 1/7] Hydra::Helper::Nix: sort exported functions --- src/lib/Hydra/Helper/Nix.pm | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/lib/Hydra/Helper/Nix.pm b/src/lib/Hydra/Helper/Nix.pm index 7113cb4a..5c0be39e 100644 --- a/src/lib/Hydra/Helper/Nix.pm +++ b/src/lib/Hydra/Helper/Nix.pm @@ -16,20 +16,31 @@ 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 + 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 { From 3a6c25489cd9c0cffbef80e0b85f2e53f2b925f6 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Fri, 21 Jan 2022 09:23:48 -0500 Subject: [PATCH 2/7] Hydra::Helper::Nix: expose a captureStdoutStderrWithStdin, make it available in tests --- src/lib/Hydra/Helper/Nix.pm | 10 ++++++++-- t/lib/CliRunners.pm | 10 ++++++++++ t/lib/Setup.pm | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lib/Hydra/Helper/Nix.pm b/src/lib/Hydra/Helper/Nix.pm index 5c0be39e..55c1f6f3 100644 --- a/src/lib/Hydra/Helper/Nix.pm +++ b/src/lib/Hydra/Helper/Nix.pm @@ -18,6 +18,7 @@ our @ISA = qw(Exporter); our @EXPORT = qw( cancelBuilds captureStdoutStderr + captureStdoutStderrWithStdin findLog gcRootFor getBaseUrl @@ -428,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/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 From bb893d0bd5dbcbbd41cc3a0b1bbc8331bff9efa2 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Fri, 21 Jan 2022 10:37:20 -0500 Subject: [PATCH 3/7] hydra-create-user: support prompting for passwords I'm not sure this is a good implementation as-is. It does work, but the password gets echo'd to the screen. I tried to use IO::Prompt but IO::Prompt really seems to want to read the password from ARGV. --- src/script/hydra-create-user | 36 ++++++++++++++++++++++++++++++++--- t/scripts/hydra-create-user.t | 24 ++++++++++++++++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user index ec14de95..839f742b 100755 --- a/src/script/hydra-create-user +++ b/src/script/hydra-create-user @@ -14,6 +14,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 +28,14 @@ renamed. * Specifying Passwords +** Interactively + +Pass `--password-prompt` to collect the password on stdin. + +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 +72,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 +81,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 +91,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 one of --password-prompt or --password-hash. See --help for more information.\n"; } die "$0: type must be `hydra', `google' or `github'\n" @@ -118,6 +128,8 @@ $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; @@ -129,6 +141,24 @@ $db->txn_do(sub { if (defined $passwordHash) { $user->setPasswordHash($passwordHash); } + + if (defined $passwordPrompt) { + print STDERR "Password: "; + my $password = // ""; + chomp $password; + + print STDERR "Password Confirmation: "; + my $passwordConfirm = // ""; + chomp $passwordConfirm; + + 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/scripts/hydra-create-user.t b/t/scripts/hydra-create-user.t index 597f4ebc..4b382acd 100644 --- a/t/scripts/hydra-create-user.t +++ b/t/scripts/hydra-create-user.t @@ -46,15 +46,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 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})); } }; From 76fbde6d6b45415b004c2468973da0cff2f3576f Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Fri, 21 Jan 2022 11:11:09 -0500 Subject: [PATCH 4/7] Set noecho when reading passwords --- flake.nix | 1 + src/script/hydra-create-user | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 43cce4bd..49f2f7a7 100644 --- a/flake.nix +++ b/flake.nix @@ -495,6 +495,7 @@ StringCompareConstantTime SysHostnameLong TermSizeAny + TermReadKey Test2Harness TestMore TestPostgreSQL diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user index 839f742b..80873ca5 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 { @@ -143,13 +144,17 @@ $db->txn_do(sub { } if (defined $passwordPrompt) { + ReadMode 2; print STDERR "Password: "; my $password = // ""; chomp $password; - print STDERR "Password Confirmation: "; + print STDERR "\nPassword Confirmation: "; my $passwordConfirm = // ""; chomp $passwordConfirm; + ReadMode 0; + + print STDERR "\n"; if ($password ne $passwordConfirm) { die "Passwords don't match." From 98928a4125c08a60f5be6febc5facbc2083f6a88 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Fri, 21 Jan 2022 12:51:30 -0500 Subject: [PATCH 5/7] fixups --- src/script/hydra-create-user | 6 ++++-- t/scripts/hydra-create-user.t | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user index 80873ca5..b9e376c8 100755 --- a/src/script/hydra-create-user +++ b/src/script/hydra-create-user @@ -33,6 +33,8 @@ renamed. 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 @@ -94,7 +96,7 @@ my $userName = $ARGV[0]; my $chosenPasswordOptions = grep { defined($_) } ($passwordPrompt, $passwordHash, $password); if ($chosenPasswordOptions > 1) { - die "$0: please specify one of --password-prompt or --password-hash. 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" @@ -135,7 +137,7 @@ $db->txn_do(sub { } else { $user->update({ emailaddress => $emailAddress }) if defined $emailAddress; - if (defined $password && !(defined $passwordHash)) { + if (defined $password) { $user->setPassword($password); } diff --git a/t/scripts/hydra-create-user.t b/t/scripts/hydra-create-user.t index 4b382acd..a182cf11 100644 --- a/t/scripts/hydra-create-user.t +++ b/t/scripts/hydra-create-user.t @@ -76,7 +76,7 @@ subtest "Handling password and password hash creation" => sub { for my $case (@cases) { my ($res, $stdout, $stderr) = captureStdoutStderr(5, ( "hydra-create-user", "bogus-password-options", @{$case})); - like($stderr, qr/please specify one of --password-prompt or --password-hash/, "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})); } }; From 0eeced7f08f4dca2413308e82640f7bf853f5ed3 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Fri, 21 Jan 2022 12:56:15 -0500 Subject: [PATCH 6/7] hydra-create-user: Warn that creating users with a plaintext password is deprecated --- src/script/hydra-create-user | 2 ++ t/scripts/hydra-create-user.t | 1 + 2 files changed, 3 insertions(+) diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user index b9e376c8..cf9e8316 100755 --- a/src/script/hydra-create-user +++ b/src/script/hydra-create-user @@ -138,6 +138,8 @@ $db->txn_do(sub { $user->update({ emailaddress => $emailAddress }) if defined $emailAddress; 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); } diff --git a/t/scripts/hydra-create-user.t b/t/scripts/hydra-create-user.t index a182cf11..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"); From da1af1ce68d56b638aa8a0fdabe00b77ff5fedd5 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Fri, 21 Jan 2022 13:05:12 -0500 Subject: [PATCH 7/7] Docs: use hydra-create-user --password-prompt --- README.md | 2 +- doc/dev-notes.txt | 2 +- doc/manual/src/installation.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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.