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 $ su - hydra
$ hydra-create-user <USER> --full-name '<NAME>' \ $ 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. 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: * Creating a user:
$ hydra-create-user root --email-address 'e.dolstra@tudelft.nl' \ $ 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.) (Replace "foobar" with the desired password.)

View file

@ -114,7 +114,7 @@ This can be done using the command `hydra-create-user`:
```console ```console
$ hydra-create-user alice --full-name 'Alice Q. User' \ $ 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. Additional users can be created through the web interface.

View file

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

View file

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

View file

@ -5,6 +5,7 @@ use warnings;
use Hydra::Schema; use Hydra::Schema;
use Hydra::Helper::Nix; use Hydra::Helper::Nix;
use Hydra::Model::DB; use Hydra::Model::DB;
use Term::ReadKey;
use Getopt::Long qw(:config gnu_getopt); use Getopt::Long qw(:config gnu_getopt);
sub showHelp { sub showHelp {
@ -14,6 +15,7 @@ Usage: hydra-create-user NAME
[--type hydra|google|github] [--type hydra|google|github]
[--full-name FULLNAME] [--full-name FULLNAME]
[--email-address EMAIL-ADDRESS] [--email-address EMAIL-ADDRESS]
[--password-prompt]
[--password-hash HASH] [--password-hash HASH]
[--password PASSWORD (dangerous)] [--password PASSWORD (dangerous)]
[--wipe-roles] [--wipe-roles]
@ -27,6 +29,16 @@ renamed.
* Specifying Passwords * 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 ** Specifying a Hash
You can generate a password hash and provide the hash as well. This You can generate a password hash and provide the hash as well. This
@ -63,7 +75,7 @@ Example:
exit 0; exit 0;
} }
my ($renameFrom, $type, $fullName, $emailAddress, $password, $passwordHash); my ($renameFrom, $type, $fullName, $emailAddress, $password, $passwordHash, $passwordPrompt);
my $wipeRoles = 0; my $wipeRoles = 0;
my @roles; my @roles;
@ -72,6 +84,7 @@ GetOptions("rename-from=s" => \$renameFrom,
"full-name=s" => \$fullName, "full-name=s" => \$fullName,
"email-address=s" => \$emailAddress, "email-address=s" => \$emailAddress,
"password=s" => \$password, "password=s" => \$password,
"password-prompt" => \$passwordPrompt,
"password-hash=s" => \$passwordHash, "password-hash=s" => \$passwordHash,
"wipe-roles" => \$wipeRoles, "wipe-roles" => \$wipeRoles,
"role=s" => \@roles, "role=s" => \@roles,
@ -81,9 +94,9 @@ GetOptions("rename-from=s" => \$renameFrom,
die "$0: one user name required\n" if scalar @ARGV != 1; die "$0: one user name required\n" if scalar @ARGV != 1;
my $userName = $ARGV[0]; my $userName = $ARGV[0];
my $chosenPasswordOptions = grep { defined($_) } ($passwordHash, $password); my $chosenPasswordOptions = grep { defined($_) } ($passwordPrompt, $passwordHash, $password);
if ($chosenPasswordOptions > 1) { 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" die "$0: type must be `hydra', `google' or `github'\n"
@ -118,17 +131,43 @@ $db->txn_do(sub {
if defined $password; if defined $password;
die "$0: Google and GitHub accounts do not have a password.\n" die "$0: Google and GitHub accounts do not have a password.\n"
if defined $passwordHash; if defined $passwordHash;
die "$0: Google and GitHub accounts do not have a password.\n"
if defined $passwordPrompt;
$user->update({ emailaddress => $userName, password => "!" }); $user->update({ emailaddress => $userName, password => "!" });
} else { } else {
$user->update({ emailaddress => $emailAddress }) if defined $emailAddress; $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); $user->setPassword($password);
} }
if (defined $passwordHash) { if (defined $passwordHash) {
$user->setPasswordHash($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; $user->userroles->delete if $wipeRoles;

View file

@ -5,6 +5,7 @@ package CliRunners;
our @ISA = qw(Exporter); our @ISA = qw(Exporter);
our @EXPORT = qw( our @EXPORT = qw(
captureStdoutStderr captureStdoutStderr
captureStdoutStderrWithStdin
evalFails evalFails
evalSucceeds evalSucceeds
runBuild runBuild
@ -21,6 +22,15 @@ sub captureStdoutStderr {
return Hydra::Helper::Nix::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 { sub evalSucceeds {
my ($jobset) = @_; my ($jobset) = @_;
my ($res, $stdout, $stderr) = captureStdoutStderr(60, ("hydra-eval-jobset", $jobset->project->name, $jobset->name)); 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 @ISA = qw(Exporter);
our @EXPORT = qw( our @EXPORT = qw(
captureStdoutStderr captureStdoutStderr
captureStdoutStderrWithStdin
createBaseJobset createBaseJobset
createJobsetWithOneInput createJobsetWithOneInput
evalFails 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 { 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")); my ($res, $stdout, $stderr) = captureStdoutStderr(5, ("hydra-create-user", "plain-text-user", "--password", "foobar"));
is($res, 0, "hydra-create-user should exit zero"); 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" }); my $user = $db->resultset('Users')->find({ username => "plain-text-user" });
isnt($user, undef, "The user exists"); 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."); 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 { subtest "Specifying conflicting password options fails" => sub {
my @cases = ( my @cases = (
[ "--password=foo", "--password-hash=8843d7f92416211de9ebb963ff4ce28125932878", "--password-prompt" ],
[ "--password=foo", "--password-prompt" ],
[ "--password=foo", "--password-hash=8843d7f92416211de9ebb963ff4ce28125932878" ], [ "--password=foo", "--password-hash=8843d7f92416211de9ebb963ff4ce28125932878" ],
[ "--password-hash=8843d7f92416211de9ebb963ff4ce28125932878", "--password-prompt" ],
); );
for my $case (@cases) { for my $case (@cases) {
my ($res, $stdout, $stderr) = captureStdoutStderr(5, ( my ($res, $stdout, $stderr) = captureStdoutStderr(5, (
"hydra-create-user", "bogus-password-options", @{$case})); "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})); isnt($res, 0, "hydra-create-user should exit non-zero with conflicting " . join(" ", @{$case}));
} }
}; };