From a77161e40a53b6100fc9cca3255638eedff5f850 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 4 Mar 2013 15:25:23 +0100 Subject: [PATCH] Allow users to edit their own settings Also, don't use the flash anymore for going back to the referer. --- src/lib/Hydra/Controller/API.pm | 18 +--- src/lib/Hydra/Controller/Admin.pm | 139 +------------------------- src/lib/Hydra/Controller/Root.pm | 37 ++----- src/lib/Hydra/Controller/User.pm | 131 ++++++++++++++++++++---- src/lib/Hydra/Helper/CatalystUtils.pm | 49 +++++++++ src/root/change-password.tt | 23 ----- src/root/topbar.tt | 2 +- src/root/user.tt | 28 +++++- src/root/users.tt | 6 +- src/script/hydra-build | 2 +- tests/Setup.pm | 2 +- tests/jobs/bzr-update.sh | 2 +- 12 files changed, 207 insertions(+), 232 deletions(-) delete mode 100644 src/root/change-password.tt diff --git a/src/lib/Hydra/Controller/API.pm b/src/lib/Hydra/Controller/API.pm index 52a1778a..1df520c6 100644 --- a/src/lib/Hydra/Controller/API.pm +++ b/src/lib/Hydra/Controller/API.pm @@ -24,16 +24,6 @@ sub api : Chained('/') PathPart('api') CaptureArgs(0) { } -sub end : ActionClass('RenderView') { - my ($self, $c) = @_; - if (scalar @{$c->error}) { - $c->stash->{json}->{error} = join "\n", @{$c->error}; - $c->forward('View::JSON'); - $c->clear_errors; - } -} - - sub projectToHash { my ($project) = @_; return { @@ -317,8 +307,6 @@ sub push : Chained('api') PathPart('push') Args(0) { , where => \ [ 'exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value = ?)', [ 'value', $r ] ] }); } - - $c->forward('View::JSON'); } @@ -326,19 +314,17 @@ sub push_github : Chained('api') PathPart('push-github') Args(0) { my ($self, $c) = @_; $c->{stash}->{json}->{jobsetsTriggered} = []; - + my $in = decode_json $c->req->body_params->{payload}; my $owner = $in->{repository}->{owner}->{name} or die; my $repo = $in->{repository}->{name} or die; print STDERR "got push from GitHub repository $owner/$repo\n"; - + triggerJobset($self, $c, $_) foreach $c->model('DB::Jobsets')->search( { 'project.enabled' => 1, 'me.enabled' => 1 }, { join => 'project' , where => \ [ 'exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value like ?)', [ 'value', "%github.com%/$owner/$repo.git%" ] ] }); - - $c->forward('View::JSON'); } diff --git a/src/lib/Hydra/Controller/Admin.pm b/src/lib/Hydra/Controller/Admin.pm index a61719aa..61835b93 100644 --- a/src/lib/Hydra/Controller/Admin.pm +++ b/src/lib/Hydra/Controller/Admin.pm @@ -8,11 +8,6 @@ use Hydra::Helper::CatalystUtils; use Hydra::Helper::AddBuilds; use Data::Dump qw(dump); use Digest::SHA1 qw(sha1_hex); -use Crypt::RandPasswd; -use Sys::Hostname::Long; -use Email::Simple; -use Email::Sender::Simple qw(sendmail); -use Email::Sender::Transport::SMTP; use Config::General; @@ -52,146 +47,13 @@ sub admin : Chained('/') PathPart('admin') CaptureArgs(0) { } -sub updateUser { - my ($c, $user) = @_; - - my $username = trim $c->request->params->{"username"}; - my $fullname = trim $c->request->params->{"fullname"}; - my $emailaddress = trim $c->request->params->{"emailaddress"}; - my $emailonerror = trim $c->request->params->{"emailonerror"}; - my $roles = $c->request->params->{"roles"} ; - - $user->update( - { fullname => $fullname - , emailaddress => $emailaddress - , emailonerror => $emailonerror - }); - $user->userroles->delete_all; - if(ref($roles) eq 'ARRAY') { - for my $s (@$roles) { - $user->userroles->create({ role => $s}) ; - } - } else { - $user->userroles->create({ role => $roles}) if defined $roles ; - } -} - - -sub create_user : Chained('admin') PathPart('create-user') Args(0) { - my ($self, $c) = @_; - - requireAdmin($c); - - $c->stash->{template} = 'user.tt'; - $c->stash->{edit} = 1; - $c->stash->{create} = 1; -} - - -sub create_user_submit : Chained('admin') PathPart('create-user/submit') Args(0) { - my ($self, $c) = @_; - - my $username = trim $c->request->params->{username}; - - txn_do($c->model('DB')->schema, sub { - my $user = $c->model('DB::Users')->create( - {username => $username, emailaddress => "", password => ""}); - updateUser($c, $user); - }); - - $c->res->redirect("/admin/users"); -} - - -sub user : Chained('admin') PathPart('user') CaptureArgs(1) { - my ($self, $c, $username) = @_; - - requireAdmin($c); - - my $user = $c->model('DB::Users')->find($username) - or notFound($c, "User $username doesn't exist."); - - $c->stash->{user} = $user; -} - - sub users : Chained('admin') PathPart('users') Args(0) { my ($self, $c) = @_; $c->stash->{users} = [$c->model('DB::Users')->search({}, {order_by => "username"})]; - $c->stash->{template} = 'users.tt'; } -sub user_edit : Chained('user') PathPart('edit') Args(0) { - my ($self, $c) = @_; - - $c->stash->{template} = 'user.tt'; - $c->stash->{edit} = 1; -} - - -sub user_edit_submit : Chained('user') PathPart('submit') Args(0) { - my ($self, $c) = @_; - requirePost($c); - - txn_do($c->model('DB')->schema, sub { - if (($c->request->params->{submit} || "") eq "delete") { - $c->stash->{user}->delete; - } else { - updateUser($c, $c->stash->{user}); - } - }); - - $c->res->redirect("/admin/users"); -} - - -sub sendemail { - my ($to, $subject, $body) = @_; - - my $url = hostname_long; - my $sender = ($ENV{'USER'} || "hydra") . "@" . $url; - - my $email = Email::Simple->create( - header => [ - To => $to, - From => "Hydra <$sender>", - Subject => $subject - ], - body => $body - ); - - sendmail($email); -} - - -sub reset_password : Chained('user') PathPart('reset-password') Args(0) { - my ($self, $c) = @_; - - # generate password - my $password = Crypt::RandPasswd->word(8,10); - - # calculate hash - my $hashed = sha1_hex($password); - - $c->stash->{user}-> update({ password => $hashed}) ; - - # send email - - sendemail( - $c->stash->{user}->emailaddress, - "New password for Hydra", - "Hi,\n\n". - "Your password has been reset. Your new password is '$password'.\n". - "You can change your password at " . $c->config()->{'base_uri'} . "/change-password .\n". - "With regards, Hydra\n" - ); - - $c->res->redirect("/admin/users"); -} - - sub machines : Chained('admin') PathPart('machines') Args(0) { my ($self, $c) = @_; $c->stash->{machines} = [$c->model('DB::BuildMachines')->search({}, {order_by => "hostname"})]; @@ -312,6 +174,7 @@ sub machine_enable : Chained('machine') PathPart('enable') Args(0) { $c->res->redirect("/admin/machines"); } + sub machine_disable : Chained('machine') PathPart('disable') Args(0) { my ($self, $c) = @_; $c->stash->{machine}->update({ enabled => 0}); diff --git a/src/lib/Hydra/Controller/Root.pm b/src/lib/Hydra/Controller/Root.pm index 61f8b4f4..6a2af005 100644 --- a/src/lib/Hydra/Controller/Root.pm +++ b/src/lib/Hydra/Controller/Root.pm @@ -156,7 +156,15 @@ sub default :Path { sub end : ActionClass('RenderView') { my ($self, $c) = @_; - if (scalar @{$c->error}) { + if (defined $c->stash->{json}) { + if (scalar @{$c->error}) { + $c->stash->{json}->{error} = join "\n", @{$c->error}; + $c->clear_errors; + } + $c->forward('View::JSON'); + } + + elsif (scalar @{$c->error}) { $c->stash->{template} = 'error.tt'; $c->stash->{errors} = $c->error; $c->response->status(500) if $c->response->status == 200; @@ -216,33 +224,6 @@ sub narinfo :LocalRegex('^([a-z0-9]+).narinfo$') :Args(0) { } -sub change_password : Path('change-password') :Args(0) { - my ($self, $c) = @_; - - requireLogin($c) if !$c->user_exists; - - $c->stash->{template} = 'change-password.tt'; -} - - -sub change_password_submit : Path('change-password/submit') : Args(0) { - my ($self, $c) = @_; - - requireLogin($c) if !$c->user_exists; - - my $password = $c->request->params->{"password"}; - my $password_check = $c->request->params->{"password_check"}; - print STDERR "$password \n"; - print STDERR "$password_check \n"; - error($c, "Passwords did not match, go back and try again!") if $password ne $password_check; - - my $hashed = sha1_hex($password); - $c->user->update({ password => $hashed}) ; - - $c->res->redirect("/"); -} - - sub logo :Local { my ($self, $c) = @_; my $path = $ENV{"HYDRA_LOGO"} or die("Logo not set!"); diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index a3032a3d..77ae643a 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -1,8 +1,10 @@ package Hydra::Controller::User; +use utf8; use strict; use warnings; use base 'Catalyst::Controller'; +use Crypt::RandPasswd; use Digest::SHA1 qw(sha1_hex); use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; @@ -17,23 +19,17 @@ sub login :Local { my $username = $c->request->params->{username} || ""; my $password = $c->request->params->{password} || ""; - if ($username eq "" && $password eq "" && !defined $c->flash->{referer}) { + if ($username eq "" && $password eq "" && !defined $c->session->{referer}) { my $baseurl = $c->uri_for('/'); - my $refurl = $c->request->referer; - $c->flash->{referer} = $refurl if $refurl =~ m/^($baseurl)/; + my $referer = $c->request->referer; + $c->session->{referer} = $referer if defined $referer && $referer =~ m/^($baseurl)/; } if ($username && $password) { - if ($c->authenticate({username => $username, password => $password})) { - $c->response->redirect($c->flash->{referer} || $c->uri_for('/')); - $c->flash->{referer} = undef; - return; - } + backToReferer($c) if $c->authenticate({username => $username, password => $password}); $c->stash->{errorMsg} = "Bad username or password."; } - $c->keep_flash("referer"); - $c->stash->{template} = 'login.tt'; } @@ -51,6 +47,18 @@ sub captcha :Local Args(0) { } +sub isValidPassword { + my ($password) = @_; + return length($password) >= 6; +} + + +sub setPassword { + my ($user, $password) = @_; + $user->update({ password => sha1_hex($password) }); +} + + sub register :Local Args(0) { my ($self, $c) = @_; @@ -81,7 +89,7 @@ sub register :Local Args(0) { return fail($c, "Your must specify your full name.") if $fullName eq ""; return fail($c, "You must specify a password of at least 6 characters.") - if length($password) < 6; + unless isValidPassword($password); return fail($c, "The passwords you specified did not match.") if $password ne trim $c->req->params->{password2}; @@ -90,22 +98,111 @@ sub register :Local Args(0) { my $user = $c->model('DB::Users')->create( { username => $userName , fullname => $fullName - , password => sha1_hex($password) + , password => "!" , emailaddress => "", }); + setPassword($user, $password); }); - $c->authenticate({username => $userName, password => $password}) - or error($c, "Unable to authenticate the new user!"); + unless ($c->user_exists) { + $c->authenticate({username => $userName, password => $password}) + or error($c, "Unable to authenticate the new user!"); + } $c->flash->{successMsg} = "User $userName has been created."; - $c->response->redirect($c->flash->{referer} || $c->uri_for('/')); + backToReferer($c); } -sub preferences :Local Args(0) { +sub user :Chained('/') PathPart('user') CaptureArgs(1) { + my ($self, $c, $userName) = @_; + + requireLogin($c) if !$c->user_exists; + + error($c, "You do not have permission to edit other users.") + if $userName ne $c->user->username && !isAdmin($c); + + $c->stash->{user} = $c->model('DB::Users')->find($userName) + or notFound($c, "User $userName doesn't exist."); +} + + +sub deleteUser { + my ($self, $c, $user) = @_; + my ($project) = $c->model('DB::Projects')->search({ owner => $user->username }); + error($c, "User " . $user->username . " is still owner of project " . $project->name . ".") + if defined $project; + $c->logout() if $user->username eq $c->user->username; + $user->delete; +} + + +sub edit :Chained('user') Args(0) { my ($self, $c) = @_; - error($c, "Not implemented."); + + my $user = $c->stash->{user}; + + $c->stash->{template} = 'user.tt'; + + $c->session->{referer} = $c->request->referer if !defined $c->session->{referer}; + + if ($c->request->method ne "POST") { + $c->stash->{fullname} = $user->fullname; + $c->stash->{emailonerror} = $user->emailonerror; + return; + } + + if (($c->request->params->{submit} // "") eq "delete") { + deleteUser($self, $c, $user); + backToReferer($c); + } + + if (($c->request->params->{submit} // "") eq "reset-password") { + $c->stash->{json} = {}; + error($c, "No email address is set for this user.") + unless $user->emailaddress; + my $password = Crypt::RandPasswd->word(8,10); + setPassword($user, $password); + sendEmail($c, + $user->emailaddress, + "Hydra password reset", + "Hi,\n\n". + "Your password has been reset. Your new password is '$password'.\n\n". + "You can change your password at " . $c->uri_for($self->action_for('edit'), [$user->username]) . ".\n\n". + "With regards,\n\nHydra.\n" + ); + return; + } + + my $fullName = trim $c->req->params->{fullname}; + + txn_do($c->model('DB')->schema, sub { + + error($c, "Your must specify your full name.") if $fullName eq ""; + + $user->update( + { fullname => $fullName + , emailonerror => $c->request->params->{"emailonerror"} ? 1 : 0 + }); + + my $password = $c->req->params->{password} // ""; + if ($password ne "") { + error($c, "You must specify a password of at least 6 characters.") + unless isValidPassword($password); + error($c, "The passwords you specified did not match.") + if $password ne trim $c->req->params->{password2}; + setPassword($user, $password); + } + + if (isAdmin($c)) { + $user->userroles->delete_all; + $user->userroles->create({ role => $_}) + foreach paramToList($c, "roles"); + } + + }); + + backToReferer($c); } diff --git a/src/lib/Hydra/Helper/CatalystUtils.pm b/src/lib/Hydra/Helper/CatalystUtils.pm index e925cd45..1cd86329 100644 --- a/src/lib/Hydra/Helper/CatalystUtils.pm +++ b/src/lib/Hydra/Helper/CatalystUtils.pm @@ -4,6 +4,10 @@ use utf8; use strict; use Exporter; use Readonly; +use Email::Simple; +use Email::Sender::Simple qw(sendmail); +use Email::Sender::Transport::SMTP; +use Sys::Hostname::Long; use Nix::Store; use Hydra::Helper::Nix; @@ -15,6 +19,9 @@ our @EXPORT = qw( trim getLatestFinishedEval parseJobsetName + sendEmail + paramToList + backToReferer $pathCompRE $relPathRE $relNameRE $projectNameRE $jobsetNameRE $jobNameRE $systemRE $userNameRE @buildListColumns ); @@ -97,8 +104,17 @@ sub notFound { } +sub backToReferer { + my ($c) = @_; + $c->response->redirect($c->session->{referer} || $c->uri_for('/')); + $c->session->{referer} = undef; + $c->detach; +} + + sub requireLogin { my ($c) = @_; + $c->session->{referer} = $c->request->uri; $c->response->redirect($c->uri_for('/login')); $c->detach; # doesn't return } @@ -162,6 +178,39 @@ sub getLatestFinishedEval { } +sub sendEmail { + my ($c, $to, $subject, $body) = @_; + + my $sender = $c->config->{'notification_sender'} || + (($ENV{'USER'} || "hydra") . "@" . hostname_long); + + my $email = Email::Simple->create( + header => [ + To => $to, + From => "Hydra <$sender>", + Subject => $subject + ], + body => $body + ); + + print STDERR "Sending email:\n", $email->as_string if $ENV{'HYDRA_MAIL_TEST'}; + + sendmail($email); +} + + +# Catalyst request parameters can be an array or a scalar or +# undefined, making them annoying to handle. So this utility function +# always returns a request parameter as a list. +sub paramToList { + my ($c, $name) = @_; + my $x = $c->request->params->{$name}; + return () unless defined $x; + return @$x if ref($x) eq 'ARRAY'; + return ($x); +} + + # Security checking of filenames. Readonly our $pathCompRE => "(?:[A-Za-z0-9-\+\._\$][A-Za-z0-9-\+\._\$]*)"; Readonly our $relPathRE => "(?:$pathCompRE(?:/$pathCompRE)*)"; diff --git a/src/root/change-password.tt b/src/root/change-password.tt deleted file mode 100644 index d6b8d5da..00000000 --- a/src/root/change-password.tt +++ /dev/null @@ -1,23 +0,0 @@ -[% WRAPPER layout.tt title="Change password" %] -[% PROCESS common.tt %] - -
- -

Change password

- - - - - - - - - - -
Enter password:
Enter password again:
- -

- -
- -[% END %] diff --git a/src/root/topbar.tt b/src/root/topbar.tt index 42b40ee9..3b66ba3d 100644 --- a/src/root/topbar.tt +++ b/src/root/topbar.tt @@ -202,7 +202,7 @@