2013-02-27 18:33:47 +01:00
|
|
|
package Hydra::Controller::User;
|
|
|
|
|
2013-03-04 15:25:23 +01:00
|
|
|
use utf8;
|
2013-02-27 18:33:47 +01:00
|
|
|
use strict;
|
|
|
|
use warnings;
|
2013-06-17 12:34:21 -04:00
|
|
|
use base 'Hydra::Base::Controller::REST';
|
2013-03-04 15:25:23 +01:00
|
|
|
use Crypt::RandPasswd;
|
2013-02-27 18:33:47 +01:00
|
|
|
use Digest::SHA1 qw(sha1_hex);
|
|
|
|
use Hydra::Helper::Nix;
|
|
|
|
use Hydra::Helper::CatalystUtils;
|
2014-11-19 14:44:04 +01:00
|
|
|
use Hydra::Helper::Email;
|
2013-07-08 23:54:40 +02:00
|
|
|
use LWP::UserAgent;
|
|
|
|
use JSON;
|
2013-07-09 12:57:34 +02:00
|
|
|
use HTML::Entities;
|
2013-02-27 18:33:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
__PACKAGE__->config->{namespace} = '';
|
|
|
|
|
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
sub login :Local :Args(0) :ActionClass('REST') { }
|
2013-06-17 12:34:21 -04:00
|
|
|
|
|
|
|
sub login_POST {
|
|
|
|
my ($self, $c) = @_;
|
|
|
|
|
2013-11-05 13:13:02 +01:00
|
|
|
my $username = $c->stash->{params}->{username} // "";
|
|
|
|
my $password = $c->stash->{params}->{password} // "";
|
|
|
|
|
|
|
|
error($c, "You must specify a user name.") if $username eq "";
|
|
|
|
error($c, "You must specify a password.") if $password eq "";
|
2013-06-17 12:34:21 -04:00
|
|
|
|
2013-11-05 13:13:02 +01:00
|
|
|
accessDenied($c, "Bad username or password.")
|
|
|
|
if !$c->authenticate({username => $username, password => $password});
|
2013-02-27 18:33:47 +01:00
|
|
|
|
2013-11-06 16:10:27 +01:00
|
|
|
currentUser_GET($self, $c);
|
2013-02-27 18:33:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
sub logout :Local :Args(0) :ActionClass('REST') { }
|
2013-06-17 12:34:21 -04:00
|
|
|
|
|
|
|
sub logout_POST {
|
2013-02-27 18:33:47 +01:00
|
|
|
my ($self, $c) = @_;
|
2013-07-09 13:55:44 +02:00
|
|
|
$c->flash->{flashMsg} = "You are no longer signed in." if $c->user_exists();
|
2013-02-27 18:33:47 +01:00
|
|
|
$c->logout;
|
2013-07-09 13:55:44 +02:00
|
|
|
$self->status_no_content($c);
|
2013-02-27 18:33:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-01-13 17:32:52 +01:00
|
|
|
sub doEmailLogin {
|
|
|
|
my ($self, $c, $type, $email, $fullName) = @_;
|
2013-07-08 23:54:40 +02:00
|
|
|
|
2016-01-13 17:32:52 +01:00
|
|
|
die "No email address provided.\n" unless defined $email;
|
2013-07-08 23:54:40 +02:00
|
|
|
|
2013-11-05 14:40:40 +01:00
|
|
|
# Be paranoid about the email address format, since we do use it
|
|
|
|
# in URLs.
|
2016-01-13 17:32:52 +01:00
|
|
|
die "Illegal email address.\n" unless $email =~ /^[a-zA-Z0-9\.\-\_]+@[a-zA-Z0-9\.\-\_]+$/;
|
2013-11-05 14:40:40 +01:00
|
|
|
|
2016-10-20 14:14:04 +02:00
|
|
|
# If allowed_domains is set, check if the email address
|
2016-01-13 17:32:52 +01:00
|
|
|
# returned is on these domains. When not configured, allow all
|
|
|
|
# domains.
|
2016-10-20 14:14:04 +02:00
|
|
|
my $allowed_domains = $c->config->{allowed_domains} // ($c->config->{persona_allowed_domains} // "");
|
2016-01-13 17:32:52 +01:00
|
|
|
if ($allowed_domains ne "") {
|
2014-01-09 13:31:02 +01:00
|
|
|
my $email_ok = 0;
|
|
|
|
my @domains = split ',', $allowed_domains;
|
|
|
|
map { $_ =~ s/^\s*(.*?)\s*$/$1/ } @domains;
|
|
|
|
|
|
|
|
foreach my $domain (@domains) {
|
|
|
|
$email_ok = $email_ok || ((split '@', $email)[1] eq $domain);
|
|
|
|
}
|
2016-01-13 17:32:52 +01:00
|
|
|
error($c, "Your email address does not belong to a domain that is allowed to log in.\n")
|
|
|
|
unless $email_ok;
|
2014-01-09 13:31:02 +01:00
|
|
|
}
|
|
|
|
|
2013-07-08 23:54:40 +02:00
|
|
|
my $user = $c->find_user({ username => $email });
|
|
|
|
|
2016-01-13 17:32:52 +01:00
|
|
|
if ($user) {
|
2016-10-20 14:14:04 +02:00
|
|
|
# Automatically upgrade legacy Persona accounts to Google accounts.
|
2016-01-13 17:32:52 +01:00
|
|
|
if ($user->type eq "persona" && $type eq "google") {
|
|
|
|
$user->update({type => "google"});
|
|
|
|
}
|
|
|
|
|
|
|
|
die "You cannot login via login type '$type'.\n" if $user->type ne $type;
|
|
|
|
} else {
|
2013-07-08 23:54:40 +02:00
|
|
|
$c->model('DB::Users')->create(
|
|
|
|
{ username => $email
|
2016-01-13 17:32:52 +01:00
|
|
|
, fullname => $fullName,
|
2013-07-08 23:54:40 +02:00
|
|
|
, password => "!"
|
|
|
|
, emailaddress => $email,
|
2016-01-13 17:32:52 +01:00
|
|
|
, type => $type
|
2013-07-08 23:54:40 +02:00
|
|
|
});
|
|
|
|
$user = $c->find_user({ username => $email }) or die;
|
|
|
|
}
|
|
|
|
|
|
|
|
$c->set_authenticated($user);
|
|
|
|
|
2013-11-05 14:10:20 +01:00
|
|
|
$self->status_no_content($c);
|
2013-07-09 13:55:44 +02:00
|
|
|
$c->flash->{successMsg} = "You are now signed in as <tt>" . encode_entities($email) . "</tt>.";
|
2013-07-08 23:54:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-01-13 17:32:52 +01:00
|
|
|
sub google_login :Path('/google-login') Args(0) {
|
|
|
|
my ($self, $c) = @_;
|
|
|
|
requirePost($c);
|
|
|
|
|
|
|
|
error($c, "Logging in via Google is not enabled.") unless $c->config->{enable_google_login};
|
|
|
|
|
2016-01-14 12:54:47 +01:00
|
|
|
my $ua = new LWP::UserAgent;
|
|
|
|
my $response = $ua->post(
|
|
|
|
'https://www.googleapis.com/oauth2/v3/tokeninfo',
|
|
|
|
{ id_token => ($c->stash->{params}->{id_token} // die "No token."),
|
|
|
|
});
|
|
|
|
error($c, "Did not get a response from Google.") unless $response->is_success;
|
|
|
|
|
|
|
|
my $data = decode_json($response->decoded_content) or die;
|
2016-01-13 17:32:52 +01:00
|
|
|
|
|
|
|
die unless $data->{aud} eq $c->config->{google_client_id};
|
|
|
|
die "Email address is not verified" unless $data->{email_verified};
|
|
|
|
# FIXME: verify hosted domain claim?
|
|
|
|
|
|
|
|
doEmailLogin($self, $c, "google", $data->{email}, $data->{name} // undef);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-02-27 18:33:47 +01:00
|
|
|
sub captcha :Local Args(0) {
|
|
|
|
my ($self, $c) = @_;
|
|
|
|
$c->create_captcha();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-03-04 15:25:23 +01:00
|
|
|
sub isValidPassword {
|
|
|
|
my ($password) = @_;
|
|
|
|
return length($password) >= 6;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub setPassword {
|
|
|
|
my ($user, $password) = @_;
|
|
|
|
$user->update({ password => sha1_hex($password) });
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-02-27 18:33:47 +01:00
|
|
|
sub register :Local Args(0) {
|
|
|
|
my ($self, $c) = @_;
|
|
|
|
|
2013-11-05 12:41:10 +01:00
|
|
|
accessDenied($c, "User registration is currently not implemented.") unless isAdmin($c);
|
2013-03-28 11:56:12 +01:00
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
if ($c->request->method eq "GET") {
|
|
|
|
$c->stash->{template} = 'user.tt';
|
|
|
|
$c->stash->{create} = 1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
die unless $c->request->method eq "PUT";
|
2013-02-27 18:33:47 +01:00
|
|
|
|
2013-12-12 09:38:46 -05:00
|
|
|
my $userName = trim $c->stash->{params}->{username};
|
2013-02-27 18:33:47 +01:00
|
|
|
$c->stash->{username} = $userName;
|
|
|
|
|
2013-11-05 12:41:10 +01:00
|
|
|
error($c, "You did not enter the correct digits from the security image.")
|
|
|
|
unless isAdmin($c) || $c->validate_captcha($c->req->param('captcha'));
|
2013-02-27 18:33:47 +01:00
|
|
|
|
2013-11-05 12:41:10 +01:00
|
|
|
error($c, "Your user name is invalid. It must start with a lower-case letter followed by lower-case letters, digits, dots or underscores.")
|
2013-02-27 18:33:47 +01:00
|
|
|
if $userName !~ /^$userNameRE$/;
|
|
|
|
|
2013-11-05 12:41:10 +01:00
|
|
|
error($c, "Your user name is already taken.")
|
2013-02-27 18:33:47 +01:00
|
|
|
if $c->find_user({ username => $userName });
|
|
|
|
|
|
|
|
txn_do($c->model('DB')->schema, sub {
|
|
|
|
my $user = $c->model('DB::Users')->create(
|
|
|
|
{ username => $userName
|
2013-03-04 15:25:23 +01:00
|
|
|
, password => "!"
|
2013-02-27 18:33:47 +01:00
|
|
|
, emailaddress => "",
|
2013-11-05 11:46:05 +01:00
|
|
|
, type => "hydra"
|
2013-02-27 18:33:47 +01:00
|
|
|
});
|
2013-11-05 12:41:10 +01:00
|
|
|
updatePreferences($c, $user);
|
2013-02-27 18:33:47 +01:00
|
|
|
});
|
|
|
|
|
2013-03-04 15:25:23 +01:00
|
|
|
unless ($c->user_exists) {
|
2013-11-05 12:41:10 +01:00
|
|
|
$c->set_authenticated({username => $userName})
|
2013-03-04 15:25:23 +01:00
|
|
|
or error($c, "Unable to authenticate the new user!");
|
|
|
|
}
|
2013-02-27 18:33:47 +01:00
|
|
|
|
|
|
|
$c->flash->{successMsg} = "User <tt>$userName</tt> has been created.";
|
2013-11-05 14:02:04 +01:00
|
|
|
$self->status_no_content($c);
|
2013-02-27 18:33:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-11-05 12:41:10 +01:00
|
|
|
sub updatePreferences {
|
|
|
|
my ($c, $user) = @_;
|
|
|
|
|
2013-12-12 09:38:46 -05:00
|
|
|
my $fullName = trim($c->stash->{params}->{fullname} // "");
|
2013-11-05 14:02:04 +01:00
|
|
|
error($c, "Your must specify your full name.") if $fullName eq "";
|
|
|
|
|
2013-12-12 09:38:46 -05:00
|
|
|
my $password = trim($c->stash->{params}->{password} // "");
|
2013-11-05 12:41:10 +01:00
|
|
|
if ($user->type eq "hydra" && ($user->password eq "!" || $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.")
|
2013-12-12 09:38:46 -05:00
|
|
|
if $password ne trim $c->stash->{params}->{password2};
|
2013-11-05 12:41:10 +01:00
|
|
|
|
|
|
|
setPassword($user, $password);
|
|
|
|
}
|
|
|
|
|
2013-12-12 09:38:46 -05:00
|
|
|
my $emailAddress = trim($c->stash->{params}->{emailaddress} // "");
|
2013-11-05 12:41:10 +01:00
|
|
|
# FIXME: validate email address?
|
|
|
|
|
|
|
|
$user->update(
|
|
|
|
{ fullname => $fullName
|
|
|
|
, emailonerror => $c->stash->{params}->{"emailonerror"} ? 1 : 0
|
2016-05-27 12:00:20 +02:00
|
|
|
, publicdashboard => $c->stash->{params}->{"publicdashboard"} ? 1 : 0
|
2013-11-05 12:41:10 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
if (isAdmin($c)) {
|
|
|
|
$user->update({ emailaddress => $emailAddress })
|
|
|
|
if $user->type eq "hydra";
|
|
|
|
|
|
|
|
$user->userroles->delete;
|
|
|
|
$user->userroles->create({ role => $_ })
|
|
|
|
foreach paramToList($c, "roles");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-06-17 12:34:21 -04:00
|
|
|
sub currentUser :Path('/current-user') :ActionClass('REST') { }
|
|
|
|
|
|
|
|
sub currentUser_GET {
|
|
|
|
my ($self, $c) = @_;
|
|
|
|
|
2013-10-14 18:01:04 +02:00
|
|
|
requireUser($c);
|
2013-06-17 12:34:21 -04:00
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
$self->status_ok($c,
|
2013-10-16 16:48:03 -04:00
|
|
|
entity => $c->model("DB::Users")->find($c->user->username)
|
2013-06-17 12:34:21 -04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-03-04 15:25:23 +01:00
|
|
|
sub user :Chained('/') PathPart('user') CaptureArgs(1) {
|
|
|
|
my ($self, $c, $userName) = @_;
|
|
|
|
|
2013-10-14 18:01:04 +02:00
|
|
|
requireUser($c);
|
2013-03-04 15:25:23 +01:00
|
|
|
|
2013-10-14 18:01:04 +02:00
|
|
|
accessDenied($c, "You do not have permission to edit other users.")
|
2013-03-04 15:25:23 +01:00
|
|
|
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.");
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
sub edit :Chained('user') :PathPart('') :Args(0) :ActionClass('REST::ForBrowsers') { }
|
2013-06-17 12:34:21 -04:00
|
|
|
|
|
|
|
sub edit_GET {
|
2013-02-27 18:33:47 +01:00
|
|
|
my ($self, $c) = @_;
|
2013-11-05 14:02:04 +01:00
|
|
|
$c->stash->{template} = 'user.tt';
|
|
|
|
}
|
2013-03-04 15:25:23 +01:00
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
sub edit_PUT {
|
|
|
|
my ($self, $c) = @_;
|
2013-03-04 15:25:23 +01:00
|
|
|
my $user = $c->stash->{user};
|
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
if (($c->stash->{params}->{submit} // "") eq "reset-password") {
|
|
|
|
return;
|
|
|
|
}
|
2013-03-04 15:25:23 +01:00
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
txn_do($c->model('DB')->schema, sub {
|
|
|
|
updatePreferences($c, $user);
|
|
|
|
});
|
2013-06-17 12:34:21 -04:00
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
$c->flash->{successMsg} = "Your preferences have been updated.";
|
|
|
|
$self->status_no_content($c);
|
2013-06-17 12:34:21 -04:00
|
|
|
}
|
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
sub edit_DELETE {
|
2013-06-17 12:34:21 -04:00
|
|
|
my ($self, $c) = @_;
|
|
|
|
my $user = $c->stash->{user};
|
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
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;
|
2013-06-17 12:34:21 -04:00
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
$c->logout() if $user->username eq $c->user->username;
|
2013-06-17 12:34:21 -04:00
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
$user->delete;
|
2013-03-04 15:25:23 +01:00
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
$c->flash->{successMsg} = "The user has been deleted.";
|
|
|
|
$self->status_no_content($c);
|
|
|
|
}
|
2013-03-04 15:25:23 +01:00
|
|
|
|
|
|
|
|
2013-11-05 14:02:04 +01:00
|
|
|
sub reset_password :Chained('user') :PathPart('reset-password') :Args(0) {
|
|
|
|
my ($self, $c) = @_;
|
|
|
|
my $user = $c->stash->{user};
|
|
|
|
|
|
|
|
requirePost($c);
|
|
|
|
|
|
|
|
error($c, "This user's password cannot be reset.") if $user->type ne "hydra";
|
|
|
|
error($c, "No email address is set for this user.")
|
|
|
|
unless $user->emailaddress;
|
|
|
|
|
|
|
|
my $password = Crypt::RandPasswd->word(8,10);
|
|
|
|
setPassword($user, $password);
|
2014-11-19 14:44:04 +01:00
|
|
|
sendEmail(
|
|
|
|
$c->config,
|
2013-11-05 14:02:04 +01:00
|
|
|
$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".
|
2014-11-19 14:44:04 +01:00
|
|
|
"With regards,\n\nHydra.\n",
|
|
|
|
[]
|
2013-11-05 14:02:04 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
$c->flash->{successMsg} = "A new password has been sent to ${\$user->emailaddress}.";
|
|
|
|
$self->status_no_content($c);
|
2013-02-27 18:33:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-05-27 12:00:20 +02:00
|
|
|
sub dashboard_old :Chained('user') :PathPart('dashboard') :Args(0) {
|
|
|
|
my ($self, $c) = @_;
|
|
|
|
$c->res->redirect($c->uri_for($self->action_for("dashboard"), $c->req->captures));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub dashboard_base :Chained('/') PathPart('dashboard') CaptureArgs(1) {
|
|
|
|
my ($self, $c, $userName) = @_;
|
|
|
|
|
|
|
|
$c->stash->{user} = $c->model('DB::Users')->find($userName)
|
|
|
|
or notFound($c, "User $userName doesn't exist.");
|
|
|
|
|
|
|
|
accessDenied($c, "You do not have permission to view this dashboard.")
|
|
|
|
unless $c->stash->{user}->publicdashboard ||
|
|
|
|
(defined $c->user && ($userName eq $c->user->username || !isAdmin($c)));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub dashboard :Chained('dashboard_base') :PathPart('') :Args(0) {
|
2013-10-14 20:07:26 +02:00
|
|
|
my ($self, $c) = @_;
|
|
|
|
$c->stash->{template} = 'dashboard.tt';
|
|
|
|
|
|
|
|
# Get the N most recent builds for each starred job.
|
|
|
|
$c->stash->{starredJobs} = [];
|
|
|
|
foreach my $j ($c->stash->{user}->starredjobs->search({}, { order_by => ['project', 'jobset', 'job'] })) {
|
|
|
|
my @builds = $j->job->builds->search(
|
|
|
|
{ },
|
|
|
|
{ rows => 20, order_by => "id desc" });
|
2014-11-25 00:27:52 +01:00
|
|
|
push @{$c->stash->{starredJobs}}, { job => $j->job, builds => [@builds] };
|
2013-10-14 20:07:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-05-27 12:00:20 +02:00
|
|
|
sub my_jobs_tab :Chained('dashboard_base') :PathPart('my-jobs-tab') :Args(0) {
|
2013-11-05 14:35:49 +01:00
|
|
|
my ($self, $c) = @_;
|
|
|
|
$c->stash->{template} = 'dashboard-my-jobs-tab.tt';
|
|
|
|
|
|
|
|
die unless $c->stash->{user}->emailaddress;
|
|
|
|
|
|
|
|
# Get all current builds of which this user is a maintainer.
|
|
|
|
$c->stash->{builds} = [$c->model('DB::Builds')->search(
|
|
|
|
{ iscurrent => 1
|
|
|
|
, maintainers => { ilike => "%" . $c->stash->{user}->emailaddress . "%" }
|
2013-11-05 14:53:52 +01:00
|
|
|
, "project.enabled" => 1
|
|
|
|
, "jobset.enabled" => 1
|
2013-11-05 14:35:49 +01:00
|
|
|
},
|
2013-11-05 14:53:52 +01:00
|
|
|
{ order_by => ["project", "jobset", "job"]
|
|
|
|
, join => ["project", "jobset"]
|
|
|
|
})];
|
2013-11-05 14:35:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-05-27 12:00:20 +02:00
|
|
|
sub my_jobsets_tab :Chained('dashboard_base') :PathPart('my-jobsets-tab') :Args(0) {
|
2013-11-05 16:05:29 +01:00
|
|
|
my ($self, $c) = @_;
|
|
|
|
$c->stash->{template} = 'dashboard-my-jobsets-tab.tt';
|
|
|
|
|
|
|
|
my $jobsets = $c->model('DB::Jobsets')->search(
|
|
|
|
{ "project.enabled" => 1, "me.enabled" => 1,
|
|
|
|
, owner => $c->stash->{user}->username
|
|
|
|
},
|
|
|
|
{ order_by => ["project", "name"]
|
|
|
|
, join => ["project"]
|
|
|
|
});
|
|
|
|
|
|
|
|
$c->stash->{jobsets} = [jobsetOverview_($c, $jobsets)];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-02-27 18:33:47 +01:00
|
|
|
1;
|