User.pm 74.3 KB
Newer Older
1
# BEGIN BPS TAGGED BLOCK {{{
Jesse Vincent's avatar
Jesse Vincent committed
2
#
3
# COPYRIGHT:
Jesse Vincent's avatar
Jesse Vincent committed
4
#
5
# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
Kevin Falcone's avatar
Kevin Falcone committed
6
#                                          <sales@bestpractical.com>
Jesse Vincent's avatar
Jesse Vincent committed
7
#
8
# (Except where explicitly superseded by other copyright notices)
Jesse Vincent's avatar
Jesse Vincent committed
9
10
#
#
11
# LICENSE:
Jesse Vincent's avatar
Jesse Vincent committed
12
#
13
14
15
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
16
# from www.gnu.org.
Jesse Vincent's avatar
Jesse Vincent committed
17
#
18
19
20
21
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
Jesse Vincent's avatar
Jesse Vincent committed
22
#
23
24
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
Jesse Vincent's avatar
Jesse Vincent committed
25
26
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
Ruslan Zakirov's avatar
Ruslan Zakirov committed
27
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
Jesse Vincent's avatar
Jesse Vincent committed
28
29
#
#
30
# CONTRIBUTION SUBMISSION POLICY:
Jesse Vincent's avatar
Jesse Vincent committed
31
#
32
33
34
35
36
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
Jesse Vincent's avatar
Jesse Vincent committed
37
#
38
39
40
41
42
43
44
45
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
Jesse Vincent's avatar
Jesse Vincent committed
46
#
47
# END BPS TAGGED BLOCK }}}
48

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
49
50
51
52
53
54
55
56
57
58
59
60
61
62
=head1 NAME

  RT::User - RT User object

=head1 SYNOPSIS

  use RT::User;

=head1 DESCRIPTION

=head1 METHODS

=cut

63
64
65

package RT::User;

66
use strict;
67
68
use warnings;

69
use Scalar::Util qw(blessed);
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
70

71
72
73
74
75
76
77
78
79
use base 'RT::Record';

sub Table {'Users'}






80
use Digest::SHA;
81
use Digest::MD5;
82
use Crypt::Eksblowfish::Bcrypt qw();
83
use RT::Principals;
84
use RT::ACE;
Alex Vandiver's avatar
Alex Vandiver committed
85
use RT::Interface::Email;
Jesse Vincent's avatar
Jesse Vincent committed
86
use Encode;
87
use Text::Password::Pronounceable;
88

89
sub _OverlayAccessible {
90
    {
91

92
          Name                  => { public => 1,  admin => 1 },    # loc_left_pair
93
          Password              => { read   => 0 },
94
95
96
97
98
          EmailAddress          => { public => 1 },                 # loc_left_pair
          Organization          => { public => 1,  admin => 1 },    # loc_left_pair
          RealName              => { public => 1 },                 # loc_left_pair
          NickName              => { public => 1 },                 # loc_left_pair
          Lang                  => { public => 1 },                 # loc_left_pair
99
100
101
102
103
104
          EmailEncoding         => { public => 1 },
          WebEncoding           => { public => 1 },
          ExternalContactInfoId => { public => 1,  admin => 1 },
          ContactInfoSystem     => { public => 1,  admin => 1 },
          ExternalAuthId        => { public => 1,  admin => 1 },
          AuthSystem            => { public => 1,  admin => 1 },
105
106
107
          Gecos                 => { public => 1,  admin => 1 },    # loc_left_pair
          PGPKey                => { public => 1,  admin => 1 },    # loc_left_pair
          SMIMECertificate      => { public => 1,  admin => 1 },    # loc_left_pair
108
          PrivateKey            => {               admin => 1 },
109
110
111
          City                  => { public => 1 },                 # loc_left_pair
          Country               => { public => 1 },                 # loc_left_pair
          Timezone              => { public => 1 },                 # loc_left_pair
112
113
114
    }
}

115

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
116

117
118
119
120
121
122
123
=head2 Create { PARAMHASH }



=cut


Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
124
125
126
127
sub Create {
    my $self = shift;
    my %args = (
        Privileged => 0,
Jesse Vincent's avatar
Jesse Vincent committed
128
        Disabled => 0,
129
        EmailAddress => '',
130
        _RecordTransaction => 1,
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
131
132
133
        @_    # get the real argumentlist
    );

134
135
136
    # remove the value so it does not cripple SUPER::Create
    my $record_transaction = delete $args{'_RecordTransaction'};

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
137
    #Check the ACL
138
    unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
139
        return ( 0, $self->loc('Permission Denied') );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
140
141
    }

142
143
144
145
146

    unless ($self->CanonicalizeUserInfo(\%args)) {
        return ( 0, $self->loc("Could not set user info") );
    }

147
    $args{'EmailAddress'} = $self->CanonicalizeEmailAddress($args{'EmailAddress'});
148

149
150
151
152
    # if the user doesn't have a name defined, set it to the email address
    $args{'Name'} = $args{'EmailAddress'} unless ($args{'Name'});


153

Ruslan Zakirov's avatar
minor    
Ruslan Zakirov committed
154
    my $privileged = delete $args{'Privileged'};
155

Jesse Vincent's avatar
Jesse Vincent committed
156
157
158
159

    if ($args{'CryptedPassword'} ) {
        $args{'Password'} = $args{'CryptedPassword'};
        delete $args{'CryptedPassword'};
160
    } elsif ( !$args{'Password'} ) {
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
161
        $args{'Password'} = '*NO-PASSWORD*';
162
    } else {
Shawn M Moore's avatar
Shawn M Moore committed
163
164
165
        my ($ok, $msg) = $self->ValidatePassword($args{'Password'});
        return ($ok, $msg) if !$ok;

166
        $args{'Password'} = $self->_GeneratePassword($args{'Password'});
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
167
168
169
170
    }

    #TODO Specify some sensible defaults.

171
    unless ( $args{'Name'} ) {
172
        return ( 0, $self->loc("Must specify 'Name' attribute") );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
173
174
    }

175
176
177
178
    my ( $val, $msg ) = $self->ValidateName( $args{'Name'} );
    return ( 0, $msg ) unless $val;
    ( $val, $msg ) = $self->ValidateEmailAddress( $args{'EmailAddress'} );
    return ( 0, $msg ) unless ($val);
179
180

    $RT::Handle->BeginTransaction();
181
182
183
184
    # Groups deal with principal ids, rather than user ids.
    # When creating this user, set up a principal Id for it.
    my $principal = RT::Principal->new($self->CurrentUser);
    my $principal_id = $principal->Create(PrincipalType => 'User',
Jesse Vincent's avatar
Jesse Vincent committed
185
                                Disabled => $args{'Disabled'},
186
187
188
189
                                ObjectId => '0');
    # If we couldn't create a principal Id, get the fuck out.
    unless ($principal_id) {
        $RT::Handle->Rollback();
190
191
        $RT::Logger->crit("Couldn't create a Principal on new user create.");
        $RT::Logger->crit("Strange things are afoot at the circle K");
192
193
        return ( 0, $self->loc('Could not create user') );
    }
194

195
    $principal->__Set(Field => 'ObjectId', Value => $principal_id);
Jesse Vincent's avatar
Jesse Vincent committed
196
    delete $args{'Disabled'};
197
198
199

    $self->SUPER::Create(id => $principal_id , %args);
    my $id = $self->Id;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
200
201
202

    #If the create failed.
    unless ($id) {
203
        $RT::Handle->Rollback();
Jesse Vincent's avatar
Jesse Vincent committed
204
        $RT::Logger->error("Could not create a new user - " .join('-', %args));
205

206
        return ( 0, $self->loc('Could not create user') );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
207
208
    }

209
210
211
212
213
    my $aclstash = RT::Group->new($self->CurrentUser);
    my $stash_id = $aclstash->_CreateACLEquivalenceGroup($principal);

    unless ($stash_id) {
        $RT::Handle->Rollback();
214
        $RT::Logger->crit("Couldn't stash the user in groupmembers");
215
216
217
        return ( 0, $self->loc('Could not create user') );
    }

218

219
    my $everyone = RT::Group->new($self->CurrentUser);
220
    $everyone->LoadSystemInternalGroup('Everyone');
221
222
223
224
225
226
    unless ($everyone->id) {
        $RT::Logger->crit("Could not load Everyone group on user creation.");
        $RT::Handle->Rollback();
        return ( 0, $self->loc('Could not create user') );
    }

227

228
    my ($everyone_id, $everyone_msg) = $everyone->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
229
230
231
232
233
234
    unless ($everyone_id) {
        $RT::Logger->crit("Could not add user to Everyone group on user creation.");
        $RT::Logger->crit($everyone_msg);
        $RT::Handle->Rollback();
        return ( 0, $self->loc('Could not create user') );
    }
235
236


237
    my $access_class = RT::Group->new($self->CurrentUser);
238
    if ($privileged)  {
239
        $access_class->LoadSystemInternalGroup('Privileged');
240
    } else {
241
        $access_class->LoadSystemInternalGroup('Unprivileged');
242
243
    }

244
245
246
247
    unless ($access_class->id) {
        $RT::Logger->crit("Could not load Privileged or Unprivileged group on user creation");
        $RT::Handle->Rollback();
        return ( 0, $self->loc('Could not create user') );
248
249
250
    }


251
    my ($ac_id, $ac_msg) = $access_class->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
252

253
254
255
256
257
258
259
260
    unless ($ac_id) {
        $RT::Logger->crit("Could not add user to Privileged or Unprivileged group on user creation. Aborted");
        $RT::Logger->crit($ac_msg);
        $RT::Handle->Rollback();
        return ( 0, $self->loc('Could not create user') );
    }


261
    if ( $record_transaction ) {
262
        $self->_NewTransaction( Type => "Create" );
263
264
    }

265
    $RT::Handle->Commit;
266

267
    return ( $id, $self->loc('User created') );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
268
269
}

270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
=head2 ValidateName STRING

Returns either (0, "failure reason") or 1 depending on whether the given
name is valid.

=cut

sub ValidateName {
    my $self = shift;
    my $name = shift;

    return ( 0, $self->loc('empty name') ) unless defined $name && length $name;

    my $TempUser = RT::User->new( RT->SystemUser );
    $TempUser->Load($name);

    if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) ) {
        return ( 0, $self->loc('Name in use') );
    }
    else {
        return 1;
    }
}

Shawn M Moore's avatar
Shawn M Moore committed
294
295
296
297
298
299
300
301
302
303
304
305
=head2 ValidatePassword STRING

Returns either (0, "failure reason") or 1 depending on whether the given
password is valid.

=cut

sub ValidatePassword {
    my $self = shift;
    my $password = shift;

    if ( length($password) < RT->Config->Get('MinimumPasswordLength') ) {
306
        return ( 0, $self->loc("Password needs to be at least [quant,_1,character,characters] long", RT->Config->Get('MinimumPasswordLength')) );
Shawn M Moore's avatar
Shawn M Moore committed
307
308
309
310
311
    }

    return 1;
}

312
313
314
=head2 SetPrivileged BOOL

If passed a true value, makes this user a member of the "Privileged"  PseudoGroup.
315
Otherwise, makes this user a member of the "Unprivileged" pseudogroup.
316
317
318
319
320
321
322
323
324
325

Returns a standard RT tuple of (val, msg);


=cut

sub SetPrivileged {
    my $self = shift;
    my $val = shift;

326
327
328
329
    #Check the ACL
    unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
        return ( 0, $self->loc('Permission Denied') );
    }
330

331
332
333
334
335
336
    $self->_SetPrivileged($val);
}

sub _SetPrivileged {
    my $self = shift;
    my $val = shift;
337
    my $priv = RT::Group->new($self->CurrentUser);
338
    $priv->LoadSystemInternalGroup('Privileged');
339
340
341
342
343
344
    unless ($priv->Id) {
        $RT::Logger->crit("Could not find Privileged pseudogroup");
        return(0,$self->loc("Failed to find 'Privileged' users pseudogroup."));
    }

    my $unpriv = RT::Group->new($self->CurrentUser);
345
    $unpriv->LoadSystemInternalGroup('Unprivileged');
346
347
348
349
350
    unless ($unpriv->Id) {
        $RT::Logger->crit("Could not find unprivileged pseudogroup");
        return(0,$self->loc("Failed to find 'Unprivileged' users pseudogroup"));
    }

351
    my $principal = $self->PrincipalId;
352
    if ($val) {
353
        if ($priv->HasMember($principal)) {
354
            #$RT::Logger->debug("That user is already privileged");
355
356
            return (0,$self->loc("That user is already privileged"));
        }
357
358
        if ($unpriv->HasMember($principal)) {
            $unpriv->_DeleteMember($principal);
359
360
361
362
363
364
365
        } else {
        # if we had layered transactions, life would be good
        # sadly, we have to just go ahead, even if something
        # bogus happened
            $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
                "unprivileged. something is drastically wrong.");
        }
366
        my ($status, $msg) = $priv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
367
368
369
370
371
        if ($status) {
            return (1, $self->loc("That user is now privileged"));
        } else {
            return (0, $msg);
        }
372
    } else {
373
        if ($unpriv->HasMember($principal)) {
374
            #$RT::Logger->debug("That user is already unprivileged");
375
376
            return (0,$self->loc("That user is already unprivileged"));
        }
377
378
        if ($priv->HasMember($principal)) {
            $priv->_DeleteMember( $principal );
379
380
381
382
383
384
385
        } else {
        # if we had layered transactions, life would be good
        # sadly, we have to just go ahead, even if something
        # bogus happened
            $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
                "unprivileged. something is drastically wrong.");
        }
386
        my ($status, $msg) = $unpriv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
387
388
389
390
391
        if ($status) {
            return (1, $self->loc("That user is now unprivileged"));
        } else {
            return (0, $msg);
        }
392
393
394
395
396
397
398
399
400
401
402
    }
}

=head2 Privileged

Returns true if this user is privileged. Returns undef otherwise.

=cut

sub Privileged {
    my $self = shift;
403
    if ( RT->PrivilegedUsers->HasMember( $self->id ) ) {
404
        return(1);
405
    } else {
406
407
408
409
        return(undef);
    }
}

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
410
411
412
#create a user without validating _any_ data.

#To be used only on database init.
Jesse Vincent's avatar
Jesse Vincent committed
413
# We can't localize here because it's before we _have_ a loc framework
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
414
415
416
417
418

sub _BootstrapCreate {
    my $self = shift;
    my %args = (@_);

Jesse Vincent's avatar
Jesse Vincent committed
419
420
    $args{'Password'} = '*NO-PASSWORD*';

421

422
    $RT::Handle->BeginTransaction();
423

424
425
426
    # Groups deal with principal ids, rather than user ids.
    # When creating this user, set up a principal Id for it.
    my $principal = RT::Principal->new($self->CurrentUser);
427
428
    my $principal_id = $principal->Create(PrincipalType => 'User', ObjectId => '0');
    $principal->__Set(Field => 'ObjectId', Value => $principal_id);
429

430
431
    # If we couldn't create a principal Id, get the fuck out.
    unless ($principal_id) {
432
        $RT::Handle->Rollback();
Jesse Vincent's avatar
Jesse Vincent committed
433
        $RT::Logger->crit("Couldn't create a Principal on new user create. Strange things are afoot at the circle K");
Jesse Vincent's avatar
Jesse Vincent committed
434
        return ( 0, 'Could not create user' );
435
    }
436
437
438
    $self->SUPER::Create(id => $principal_id, %args);
    my $id = $self->Id;
    #If the create failed.
439
440
441
442
      unless ($id) {
      $RT::Handle->Rollback();
      return ( 0, 'Could not create user' ) ; #never loc this
    }
443
444
445
446
447
448
449
450
451
452

    my $aclstash = RT::Group->new($self->CurrentUser);
    my $stash_id  = $aclstash->_CreateACLEquivalenceGroup($principal);

    unless ($stash_id) {
        $RT::Handle->Rollback();
        $RT::Logger->crit("Couldn't stash the user in groupmembers");
        return ( 0, $self->loc('Could not create user') );
    }

453
    $RT::Handle->Commit();
454

Jesse Vincent's avatar
Jesse Vincent committed
455
    return ( $id, 'User created' );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
456
457
458
459
460
}

sub Delete {
    my $self = shift;

461
    return ( 0, $self->loc('Deleting this object would violate referential integrity') );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
462
463
464
465
466
467

}

=head2 Load

Load a user object from the database. Takes a single argument.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
468
469
470
471
If the argument is numerical, load by the column 'id'. If a user
object or its subclass passed then loads the same user by id.
Otherwise, load by the "Name" column which is the user's textual
username.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
472
473
474
475

=cut

sub Load {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
476
    my $self = shift;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
477
478
479
    my $identifier = shift || return undef;

    if ( $identifier !~ /\D/ ) {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
480
        return $self->SUPER::LoadById( $identifier );
481
    } elsif ( UNIVERSAL::isa( $identifier, 'RT::User' ) ) {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
482
        return $self->SUPER::LoadById( $identifier->Id );
483
    } else {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
484
        return $self->LoadByCol( "Name", $identifier );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
    }
}

=head2 LoadByEmail

Tries to load this user object from the database by the user's email address.

=cut

sub LoadByEmail {
    my $self    = shift;
    my $address = shift;

    # Never load an empty address as an email address.
    unless ($address) {
        return (undef);
    }

    $address = $self->CanonicalizeEmailAddress($address);

505
    #$RT::Logger->debug("Trying to load an email address: $address");
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
506
507
508
    return $self->LoadByCol( "EmailAddress", $address );
}

509
510
511
=head2 LoadOrCreateByEmail ADDRESS

Attempts to find a user who has the provided email address. If that fails, creates an unprivileged user with
Emmanuel Lacour's avatar
Emmanuel Lacour committed
512
the provided email address and loads them. Address can be provided either as L<Email::Address> object
Ruslan Zakirov's avatar
Ruslan Zakirov committed
513
or string which is parsed using the module.
514
515
516
517
518
519
520
521
522
523

Returns a tuple of the user's id and a status message.
0 will be returned in place of the user's id in case of failure.

=cut

sub LoadOrCreateByEmail {
    my $self = shift;
    my $email = shift;

524
    my ($message, $name);
Emmanuel Lacour's avatar
Emmanuel Lacour committed
525
    if ( UNIVERSAL::isa( $email => 'Email::Address' ) ) {
526
527
        ($email, $name) = ($email->address, $email->phrase);
    } else {
528
        ($email, $name) = RT::Interface::Email::ParseAddressFromHeader( $email );
529
530
531
    }

    $self->LoadByEmail( $email );
532
    $self->Load( $email ) unless $self->Id;
533
534
    $message = $self->loc('User loaded');

535
    unless( $self->Id ) {
536
537
538
539
        my $val;
        ($val, $message) = $self->Create(
            Name         => $email,
            EmailAddress => $email,
Ruslan Zakirov's avatar
Ruslan Zakirov committed
540
            RealName     => $name,
541
542
543
544
545
546
547
548
549
550
551
552
553
            Privileged   => 0,
            Comments     => 'Autocreated when added as a watcher',
        );
        unless ( $val ) {
            # Deal with the race condition of two account creations at once
            $self->LoadByEmail( $email );
            unless ( $self->Id ) {
                sleep 5;
                $self->LoadByEmail( $email );
            }
            if ( $self->Id ) {
                $RT::Logger->error("Recovered from creation failure due to race condition");
                $message = $self->loc("User loaded");
554
            } else {
555
                $RT::Logger->crit("Failed to create user ". $email .": " .$message);
556
557
558
            }
        }
    }
559
560
    return wantarray ? (0, $message) : 0 unless $self->id;
    return wantarray ? ($self->Id, $message) : $self->Id;
561
}
562

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
563
564
=head2 ValidateEmailAddress ADDRESS

565
566
Returns true if the email address entered is not in use by another user or is
undef or ''. Returns false if it's in use.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
567
568
569
570
571
572
573
574
575
576

=cut

sub ValidateEmailAddress {
    my $self  = shift;
    my $Value = shift;

    # if the email address is null, it's always valid
    return (1) if ( !$Value || $Value eq "" );

577
578
579
580
581
    if ( RT->Config->Get('ValidateUserEmailAddresses') ) {
        # We only allow one valid email address
        my @addresses = Email::Address->parse($Value);
        return ( 0, $self->loc('Invalid syntax for email address') ) unless ( ( scalar (@addresses) == 1 ) && ( $addresses[0]->address ) );
    }
582
583


584
    my $TempUser = RT::User->new(RT->SystemUser);
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
585
586
    $TempUser->LoadByEmail($Value);

587
    if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) )
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
588
589
    {    # if we found a user with that address
            # it's invalid to set this user's address to it
590
        return ( 0, $self->loc('Email address in use') );
591
    } else {    #it's a valid email address
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
592
593
594
595
        return (1);
    }
}

596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
=head2 SetName

Check to make sure someone else isn't using this name already

=cut

sub SetName {
    my $self  = shift;
    my $Value = shift;

    my ( $val, $message ) = $self->ValidateName($Value);
    if ($val) {
        return $self->_Set( Field => 'Name', Value => $Value );
    }
    else {
        return ( 0, $message );
    }
}

615
616
617
618
619
620
621
622
=head2 SetEmailAddress

Check to make sure someone else isn't using this email address already
so that a better email address can be returned

=cut

sub SetEmailAddress {
623
    my $self  = shift;
624
    my $Value = shift;
625
    $Value = '' unless defined $Value;
626

627
628
    my ($val, $message) = $self->ValidateEmailAddress( $Value );
    if ( $val ) {
629
630
        return $self->_Set( Field => 'EmailAddress', Value => $Value );
    } else {
631
        return ( 0, $message )
632
633
634
635
    }

}

636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
=head2 EmailFrequency

Takes optional Ticket argument in paramhash. Returns 'no email',
'squelched', 'daily', 'weekly' or empty string depending on
user preferences.

=over 4

=item 'no email' - user has no email, so can not recieve notifications.

=item 'squelched' - returned only when Ticket argument is provided and
notifications to the user has been supressed for this ticket.

=item 'daily' - retruned when user recieve daily messages digest instead
of immediate delivery.

=item 'weekly' - previous, but weekly.

=item empty string returned otherwise.

=back

=cut

sub EmailFrequency {
    my $self = shift;
    my %args = (
        Ticket => undef,
        @_
    );
666
667
    return '' unless $self->id && $self->id != RT->Nobody->id
        && $self->id != RT->SystemUser->id;
668
669
    return 'no email address' unless my $email = $self->EmailAddress;
    return 'email disabled for ticket' if $args{'Ticket'} &&
670
671
672
673
674
675
676
        grep lc $email eq lc $_->Content, $args{'Ticket'}->SquelchMailTo;
    my $frequency = RT->Config->Get( 'EmailFrequency', $self ) || '';
    return 'daily' if $frequency =~ /daily/i;
    return 'weekly' if $frequency =~ /weekly/i;
    return '';
}

677
=head2 CanonicalizeEmailAddress ADDRESS
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
678

679
680
CanonicalizeEmailAddress converts email addresses into canonical form.
it takes one email address in and returns the proper canonical
681
form. You can dump whatever your proper local config is in here.  Note
Ruslan Zakirov's avatar
Ruslan Zakirov committed
682
683
that it may be called as a static method; in this case the first argument
is class name not an object.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
684
685
686
687
688
689
690
691
692

=cut

sub CanonicalizeEmailAddress {
    my $self = shift;
    my $email = shift;
    # Example: the following rule would treat all email
    # coming from a subdomain as coming from second level domain
    # foo.com
693
694
695
696
    if ( my $match   = RT->Config->Get('CanonicalizeEmailAddressMatch') and
         my $replace = RT->Config->Get('CanonicalizeEmailAddressReplace') )
    {
        $email =~ s/$match/$replace/gi;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
697
698
699
700
    }
    return ($email);
}

701
=head2 CanonicalizeUserInfo HASH of ARGS
702

703
704
705
CanonicalizeUserInfo can convert all User->Create options.
it takes a hashref of all the params sent to User->Create and
returns that same hash, by default nothing is done.
706

707
708
This function is intended to allow users to have their info looked up via
an outside source and modified upon creation.
709
710
711
712
713
714
715
716
717
718
719
720

=cut

sub CanonicalizeUserInfo {
    my $self = shift;
    my $args = shift;
    my $success = 1;

    return ($success);
}


721
=head2 Password and authentication related functions
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
722

Ruslan Zakirov's avatar
Ruslan Zakirov committed
723
=head3 SetRandomPassword
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
724
725
726
727
728
729
730
731
732
733
734

Takes no arguments. Returns a status code and a new password or an error message.
If the status is 1, the second value returned is the new password.
If the status is anything else, the new value returned is the error code.

=cut

sub SetRandomPassword {
    my $self = shift;

    unless ( $self->CurrentUserCanModify('Password') ) {
735
        return ( 0, $self->loc("Permission Denied") );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
736
737
    }

Jesse Vincent's avatar
Jesse Vincent committed
738

739
740
    my $min = ( RT->Config->Get('MinimumPasswordLength') > 6 ?  RT->Config->Get('MinimumPasswordLength') : 6);
    my $max = ( RT->Config->Get('MinimumPasswordLength') > 8 ?  RT->Config->Get('MinimumPasswordLength') : 8);
Jesse Vincent's avatar
Jesse Vincent committed
741
742

    my $pass = $self->GenerateRandomPassword( $min, $max) ;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
743

744
    # If we have "notify user on
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
745
746
747
748
749
750
751
752
753
754
755

    my ( $val, $msg ) = $self->SetPassword($pass);

    #If we got an error return the error.
    return ( 0, $msg ) unless ($val);

    #Otherwise, we changed the password, lets return it.
    return ( 1, $pass );

}

Ruslan Zakirov's avatar
Ruslan Zakirov committed
756
=head3 ResetPassword
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
757

758
759
Returns status, [ERROR or new password].  Resets this user's password to
a randomly generated pronouncable password and emails them, using a
760
761
762
global template called "PasswordChange".

This function is currently unused in the UI, but available for local scripts.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
763
764
765
766
767
768
769

=cut

sub ResetPassword {
    my $self = shift;

    unless ( $self->CurrentUserCanModify('Password') ) {
770
        return ( 0, $self->loc("Permission Denied") );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
771
772
773
774
775
776
777
    }
    my ( $status, $pass ) = $self->SetRandomPassword();

    unless ($status) {
        return ( 0, "$pass" );
    }

778
    my $ret = RT::Interface::Email::SendEmailUsingTemplate(
779
780
781
782
783
        To        => $self->EmailAddress,
        Template  => 'PasswordChange',
        Arguments => {
            NewPassword => $pass,
        },
784
    );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
785
786

    if ($ret) {
787
        return ( 1, $self->loc('New password notification sent') );
788
    } else {
789
        return ( 0, $self->loc('Notification could not be sent') );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
790
791
792
793
    }

}

Ruslan Zakirov's avatar
Ruslan Zakirov committed
794
=head3 GenerateRandomPassword MIN_LEN and MAX_LEN
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
795
796
797
798
799
800

Returns a random password between MIN_LEN and MAX_LEN characters long.

=cut

sub GenerateRandomPassword {
801
802
    my $self = shift;   # just to drop it
    return Text::Password::Pronounceable->generate(@_);
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
803
804
}

805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
sub SafeSetPassword {
    my $self = shift;
    my %args = (
        Current      => undef,
        New          => undef,
        Confirmation => undef,
        @_,
    );
    return (1) unless defined $args{'New'} && length $args{'New'};

    my %cond = $self->CurrentUserRequireToSetPassword;

    unless ( $cond{'CanSet'} ) {
        return (0, $self->loc('You can not set password.') .' '. $cond{'Reason'} );
    }

821
    my $error = '';
822
823
824
    if ( $cond{'RequireCurrent'} && !$self->CurrentUser->IsPassword($args{'Current'}) ) {
        if ( defined $args{'Current'} && length $args{'Current'} ) {
            $error = $self->loc("Please enter your current password correctly.");
825
        } else {
826
827
828
829
830
831
832
833
834
835
836
837
838
839
            $error = $self->loc("Please enter your current password.");
        }
    } elsif ( $args{'New'} ne $args{'Confirmation'} ) {
        $error = $self->loc("Passwords do not match.");
    }

    if ( $error ) {
        $error .= ' '. $self->loc('Password has not been set.');
        return (0, $error);
    }

    return $self->SetPassword( $args{'New'} );
}

Ruslan Zakirov's avatar
Ruslan Zakirov committed
840
=head3 SetPassword
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
841

842
Takes a string. Checks the string's length and sets this user's password
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
843
844
845
846
847
848
849
850
851
to that string.

=cut

sub SetPassword {
    my $self     = shift;
    my $password = shift;

    unless ( $self->CurrentUserCanModify('Password') ) {
Jesse Vincent's avatar
Jesse Vincent committed
852
        return ( 0, $self->loc('Password: Permission Denied') );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
853
854
855
    }

    if ( !$password ) {
856
        return ( 0, $self->loc("No password set") );
857
    } else {
Shawn M Moore's avatar
Shawn M Moore committed
858
859
860
        my ($val, $msg) = $self->ValidatePassword($password);
        return ($val, $msg) if !$val;

Jesse Vincent's avatar
Jesse Vincent committed
861
        my $new = !$self->HasPassword;
862
        $password = $self->_GeneratePassword($password);
Shawn M Moore's avatar
Shawn M Moore committed
863
864

        ( $val, $msg ) = $self->_Set(Field => 'Password', Value => $password);
865
        if ($val) {
Jesse Vincent's avatar
Jesse Vincent committed
866
            return ( 1, $self->loc("Password set") ) if $new;
867
            return ( 1, $self->loc("Password changed") );
868
        } else {
869
870
            return ( $val, $msg );
        }
871
872
873
874
    }

}

875
876
877
878
879
880
881
882
883
884
885
886
887
888
sub _GeneratePassword_bcrypt {
    my $self = shift;
    my ($password, @rest) = @_;

    my $salt;
    my $rounds;
    if (@rest) {
        # The first split is the number of rounds
        $rounds = $rest[0];

        # The salt is the first 22 characters, b64 encoded usign the
        # special bcrypt base64.
        $salt = Crypt::Eksblowfish::Bcrypt::de_base64( substr($rest[1], 0, 22) );
    } else {
889
        $rounds = RT->Config->Get('BcryptCost');
890
891
892
893
894
895
896
897
898
899

        # Generate a random 16-octet base64 salt
        $salt = "";
        $salt .= pack("C", int rand(256)) for 1..16;
    }

    my $hash = Crypt::Eksblowfish::Bcrypt::bcrypt_hash({
        key_nul => 1,
        cost    => $rounds,
        salt    => $salt,
900
    }, Digest::SHA::sha512( Encode::encode( 'UTF-8', $password) ) );
901
902
903
904
905
906
907

    return join("!", "", "bcrypt", sprintf("%02d", $rounds),
                Crypt::Eksblowfish::Bcrypt::en_base64( $salt ).
                Crypt::Eksblowfish::Bcrypt::en_base64( $hash )
              );
}

908
sub _GeneratePassword_sha512 {
909
    my $self = shift;
910
    my ($password, $salt) = @_;
911

912
913
914
915
916
917
    # Generate a 16-character base64 salt
    unless ($salt) {
        $salt = "";
        $salt .= ("a".."z", "A".."Z","0".."9", "+", "/")[rand 64]
            for 1..16;
    }
918

919
920
    my $sha = Digest::SHA->new(512);
    $sha->add($salt);
921
    $sha->add(Encode::encode( 'UTF-8', $password));
922
    return join("!", "", "sha512", $salt, $sha->b64digest);
923
924
}

925
=head3 _GeneratePassword PASSWORD [, SALT]
926

927
Returns a string to store in the database.  This string takes the form:
928

929
   !method!salt!hash
930

931
By default, the method is currently C<bcrypt>.
932

933
=cut
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
934

935
936
sub _GeneratePassword {
    my $self = shift;
937
    return $self->_GeneratePassword_bcrypt(@_);
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
938
939
}

Ruslan Zakirov's avatar
Ruslan Zakirov committed
940
=head3 HasPassword
941
942
943

Returns true if the user has a valid password, otherwise returns false.

Jesse Vincent's avatar
Jesse Vincent committed
944
945
946
947
=cut

sub HasPassword {
    my $self = shift;
Ruslan Zakirov's avatar
Ruslan Zakirov committed
948
949
950
951
    my $pwd = $self->__Value('Password');
    return undef if !defined $pwd
                    || $pwd eq ''
                    || $pwd eq '*NO-PASSWORD*';
Jesse Vincent's avatar
Jesse Vincent committed
952
953
954
    return 1;
}

Ruslan Zakirov's avatar
Ruslan Zakirov committed
955
=head3 IsPassword
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
956
957
958
959
960
961
962
963
964
965
966
967

Returns true if the passed in value is this user's password.
Returns undef otherwise.

=cut

sub IsPassword {
    my $self  = shift;
    my $value = shift;

    #TODO there isn't any apparent way to legitimately ACL this

968
    # RT does not allow null passwords
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
969
970
971
    if ( ( !defined($value) ) or ( $value eq '' ) ) {
        return (undef);
    }
972
973

   if ( $self->PrincipalObj->Disabled ) {
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
974
975
976
977
        $RT::Logger->info(
            "Disabled user " . $self->Name . " tried to log in" );
        return (undef);
    }
Jesse Vincent's avatar
Jesse Vincent committed
978

Jesse Vincent's avatar
Jesse Vincent committed
979
    unless ($self->HasPassword) {
Jesse Vincent's avatar
Jesse Vincent committed
980
981
982
        return(undef);
     }

983
984
985
    my $stored = $self->__Value('Password');
    if ($stored =~ /^!/) {
        # If it's a new-style (>= RT 4.0) password, it starts with a '!'
986
987
        my (undef, $method, @rest) = split /!/, $stored;
        if ($method eq "bcrypt") {
988
989
990
            return 0 unless $self->_GeneratePassword_bcrypt($value, @rest) eq $stored;
            # Upgrade to a larger number of rounds if necessary
            return 1 unless $rest[0] < RT->Config->Get('BcryptCost');
991
992
        } elsif ($method eq "sha512") {
            return 0 unless $self->_GeneratePassword_sha512($value, @rest) eq $stored;
993
994
995
996
997
998
999
        } else {
            $RT::Logger->warn("Unknown hash method $method");
            return 0;
        }
    } elsif (length $stored == 40) {
        # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long
        my $hash = MIME::Base64::decode_base64($stored);
1000
        # Decoding yields 30 byes; first 4 are the salt, the rest are substr(SHA256,0,26)