00b230fbd02afa38e13a505e72ebd70255f9e62c
[usit-rt.git] / lib / RT / User.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::User - RT User object
52
53 =head1 SYNOPSIS
54
55   use RT::User;
56
57 =head1 DESCRIPTION
58
59 =head1 METHODS
60
61 =cut
62
63
64 package RT::User;
65
66 use strict;
67 use warnings;
68
69
70 use base 'RT::Record';
71
72 sub Table {'Users'}
73
74
75
76
77
78
79 use Digest::SHA;
80 use Digest::MD5;
81 use RT::Principals;
82 use RT::ACE;
83 use RT::Interface::Email;
84 use Encode;
85 use Text::Password::Pronounceable;
86
87 sub _OverlayAccessible {
88     {
89
90         Name                    => { public => 1,  admin => 1 },
91           Password              => { read   => 0 },
92           EmailAddress          => { public => 1 },
93           Organization          => { public => 1,  admin => 1 },
94           RealName              => { public => 1 },
95           NickName              => { public => 1 },
96           Lang                  => { public => 1 },
97           EmailEncoding         => { public => 1 },
98           WebEncoding           => { public => 1 },
99           ExternalContactInfoId => { public => 1,  admin => 1 },
100           ContactInfoSystem     => { public => 1,  admin => 1 },
101           ExternalAuthId        => { public => 1,  admin => 1 },
102           AuthSystem            => { public => 1,  admin => 1 },
103           Gecos                 => { public => 1,  admin => 1 },
104           PGPKey                => { public => 1,  admin => 1 },
105
106     }
107 }
108
109
110
111 =head2 Create { PARAMHASH }
112
113
114
115 =cut
116
117
118 sub Create {
119     my $self = shift;
120     my %args = (
121         Privileged => 0,
122         Disabled => 0,
123         EmailAddress => '',
124         _RecordTransaction => 1,
125         @_    # get the real argumentlist
126     );
127
128     # remove the value so it does not cripple SUPER::Create
129     my $record_transaction = delete $args{'_RecordTransaction'};
130
131     #Check the ACL
132     unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
133         return ( 0, $self->loc('Permission Denied') );
134     }
135
136
137     unless ($self->CanonicalizeUserInfo(\%args)) {
138         return ( 0, $self->loc("Could not set user info") );
139     }
140
141     $args{'EmailAddress'} = $self->CanonicalizeEmailAddress($args{'EmailAddress'});
142
143     # if the user doesn't have a name defined, set it to the email address
144     $args{'Name'} = $args{'EmailAddress'} unless ($args{'Name'});
145
146
147
148     my $privileged = delete $args{'Privileged'};
149
150
151     if ($args{'CryptedPassword'} ) {
152         $args{'Password'} = $args{'CryptedPassword'};
153         delete $args{'CryptedPassword'};
154     } elsif ( !$args{'Password'} ) {
155         $args{'Password'} = '*NO-PASSWORD*';
156     } else {
157         my ($ok, $msg) = $self->ValidatePassword($args{'Password'});
158         return ($ok, $msg) if !$ok;
159
160         $args{'Password'} = $self->_GeneratePassword($args{'Password'});
161     }
162
163     #TODO Specify some sensible defaults.
164
165     unless ( $args{'Name'} ) {
166         return ( 0, $self->loc("Must specify 'Name' attribute") );
167     }
168
169     #SANITY CHECK THE NAME AND ABORT IF IT'S TAKEN
170     if (RT->SystemUser) {   #This only works if RT::SystemUser has been defined
171         my $TempUser = RT::User->new(RT->SystemUser);
172         $TempUser->Load( $args{'Name'} );
173         return ( 0, $self->loc('Name in use') ) if ( $TempUser->Id );
174
175         my ($val, $message) = $self->ValidateEmailAddress( $args{'EmailAddress'} );
176         return (0, $message) unless ( $val );
177     } else {
178         $RT::Logger->warning( "$self couldn't check for pre-existing users");
179     }
180
181
182     $RT::Handle->BeginTransaction();
183     # Groups deal with principal ids, rather than user ids.
184     # When creating this user, set up a principal Id for it.
185     my $principal = RT::Principal->new($self->CurrentUser);
186     my $principal_id = $principal->Create(PrincipalType => 'User',
187                                 Disabled => $args{'Disabled'},
188                                 ObjectId => '0');
189     # If we couldn't create a principal Id, get the fuck out.
190     unless ($principal_id) {
191         $RT::Handle->Rollback();
192         $RT::Logger->crit("Couldn't create a Principal on new user create.");
193         $RT::Logger->crit("Strange things are afoot at the circle K");
194         return ( 0, $self->loc('Could not create user') );
195     }
196
197     $principal->__Set(Field => 'ObjectId', Value => $principal_id);
198     delete $args{'Disabled'};
199
200     $self->SUPER::Create(id => $principal_id , %args);
201     my $id = $self->Id;
202
203     #If the create failed.
204     unless ($id) {
205         $RT::Handle->Rollback();
206         $RT::Logger->error("Could not create a new user - " .join('-', %args));
207
208         return ( 0, $self->loc('Could not create user') );
209     }
210
211     my $aclstash = RT::Group->new($self->CurrentUser);
212     my $stash_id = $aclstash->_CreateACLEquivalenceGroup($principal);
213
214     unless ($stash_id) {
215         $RT::Handle->Rollback();
216         $RT::Logger->crit("Couldn't stash the user in groupmembers");
217         return ( 0, $self->loc('Could not create user') );
218     }
219
220
221     my $everyone = RT::Group->new($self->CurrentUser);
222     $everyone->LoadSystemInternalGroup('Everyone');
223     unless ($everyone->id) {
224         $RT::Logger->crit("Could not load Everyone group on user creation.");
225         $RT::Handle->Rollback();
226         return ( 0, $self->loc('Could not create user') );
227     }
228
229
230     my ($everyone_id, $everyone_msg) = $everyone->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
231     unless ($everyone_id) {
232         $RT::Logger->crit("Could not add user to Everyone group on user creation.");
233         $RT::Logger->crit($everyone_msg);
234         $RT::Handle->Rollback();
235         return ( 0, $self->loc('Could not create user') );
236     }
237
238
239     my $access_class = RT::Group->new($self->CurrentUser);
240     if ($privileged)  {
241         $access_class->LoadSystemInternalGroup('Privileged');
242     } else {
243         $access_class->LoadSystemInternalGroup('Unprivileged');
244     }
245
246     unless ($access_class->id) {
247         $RT::Logger->crit("Could not load Privileged or Unprivileged group on user creation");
248         $RT::Handle->Rollback();
249         return ( 0, $self->loc('Could not create user') );
250     }
251
252
253     my ($ac_id, $ac_msg) = $access_class->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
254
255     unless ($ac_id) {
256         $RT::Logger->crit("Could not add user to Privileged or Unprivileged group on user creation. Aborted");
257         $RT::Logger->crit($ac_msg);
258         $RT::Handle->Rollback();
259         return ( 0, $self->loc('Could not create user') );
260     }
261
262
263     if ( $record_transaction ) {
264         $self->_NewTransaction( Type => "Create" );
265     }
266
267     $RT::Handle->Commit;
268
269     return ( $id, $self->loc('User created') );
270 }
271
272 =head2 ValidatePassword STRING
273
274 Returns either (0, "failure reason") or 1 depending on whether the given
275 password is valid.
276
277 =cut
278
279 sub ValidatePassword {
280     my $self = shift;
281     my $password = shift;
282
283     if ( length($password) < RT->Config->Get('MinimumPasswordLength') ) {
284         return ( 0, $self->loc("Password needs to be at least [_1] characters long", RT->Config->Get('MinimumPasswordLength')) );
285     }
286
287     return 1;
288 }
289
290 =head2 SetPrivileged BOOL
291
292 If passed a true value, makes this user a member of the "Privileged"  PseudoGroup.
293 Otherwise, makes this user a member of the "Unprivileged" pseudogroup.
294
295 Returns a standard RT tuple of (val, msg);
296
297
298 =cut
299
300 sub SetPrivileged {
301     my $self = shift;
302     my $val = shift;
303
304     #Check the ACL
305     unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
306         return ( 0, $self->loc('Permission Denied') );
307     }
308
309     $self->_SetPrivileged($val);
310 }
311
312 sub _SetPrivileged {
313     my $self = shift;
314     my $val = shift;
315     my $priv = RT::Group->new($self->CurrentUser);
316     $priv->LoadSystemInternalGroup('Privileged');
317     unless ($priv->Id) {
318         $RT::Logger->crit("Could not find Privileged pseudogroup");
319         return(0,$self->loc("Failed to find 'Privileged' users pseudogroup."));
320     }
321
322     my $unpriv = RT::Group->new($self->CurrentUser);
323     $unpriv->LoadSystemInternalGroup('Unprivileged');
324     unless ($unpriv->Id) {
325         $RT::Logger->crit("Could not find unprivileged pseudogroup");
326         return(0,$self->loc("Failed to find 'Unprivileged' users pseudogroup"));
327     }
328
329     my $principal = $self->PrincipalId;
330     if ($val) {
331         if ($priv->HasMember($principal)) {
332             #$RT::Logger->debug("That user is already privileged");
333             return (0,$self->loc("That user is already privileged"));
334         }
335         if ($unpriv->HasMember($principal)) {
336             $unpriv->_DeleteMember($principal);
337         } else {
338         # if we had layered transactions, life would be good
339         # sadly, we have to just go ahead, even if something
340         # bogus happened
341             $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
342                 "unprivileged. something is drastically wrong.");
343         }
344         my ($status, $msg) = $priv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
345         if ($status) {
346             return (1, $self->loc("That user is now privileged"));
347         } else {
348             return (0, $msg);
349         }
350     } else {
351         if ($unpriv->HasMember($principal)) {
352             #$RT::Logger->debug("That user is already unprivileged");
353             return (0,$self->loc("That user is already unprivileged"));
354         }
355         if ($priv->HasMember($principal)) {
356             $priv->_DeleteMember( $principal );
357         } else {
358         # if we had layered transactions, life would be good
359         # sadly, we have to just go ahead, even if something
360         # bogus happened
361             $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
362                 "unprivileged. something is drastically wrong.");
363         }
364         my ($status, $msg) = $unpriv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
365         if ($status) {
366             return (1, $self->loc("That user is now unprivileged"));
367         } else {
368             return (0, $msg);
369         }
370     }
371 }
372
373 =head2 Privileged
374
375 Returns true if this user is privileged. Returns undef otherwise.
376
377 =cut
378
379 sub Privileged {
380     my $self = shift;
381     if ( RT->PrivilegedUsers->HasMember( $self->id ) ) {
382         return(1);
383     } else {
384         return(undef);
385     }
386 }
387
388 #create a user without validating _any_ data.
389
390 #To be used only on database init.
391 # We can't localize here because it's before we _have_ a loc framework
392
393 sub _BootstrapCreate {
394     my $self = shift;
395     my %args = (@_);
396
397     $args{'Password'} = '*NO-PASSWORD*';
398
399
400     $RT::Handle->BeginTransaction();
401
402     # Groups deal with principal ids, rather than user ids.
403     # When creating this user, set up a principal Id for it.
404     my $principal = RT::Principal->new($self->CurrentUser);
405     my $principal_id = $principal->Create(PrincipalType => 'User', ObjectId => '0');
406     $principal->__Set(Field => 'ObjectId', Value => $principal_id);
407
408     # If we couldn't create a principal Id, get the fuck out.
409     unless ($principal_id) {
410         $RT::Handle->Rollback();
411         $RT::Logger->crit("Couldn't create a Principal on new user create. Strange things are afoot at the circle K");
412         return ( 0, 'Could not create user' );
413     }
414     $self->SUPER::Create(id => $principal_id, %args);
415     my $id = $self->Id;
416     #If the create failed.
417       unless ($id) {
418       $RT::Handle->Rollback();
419       return ( 0, 'Could not create user' ) ; #never loc this
420     }
421
422     my $aclstash = RT::Group->new($self->CurrentUser);
423     my $stash_id  = $aclstash->_CreateACLEquivalenceGroup($principal);
424
425     unless ($stash_id) {
426         $RT::Handle->Rollback();
427         $RT::Logger->crit("Couldn't stash the user in groupmembers");
428         return ( 0, $self->loc('Could not create user') );
429     }
430
431     $RT::Handle->Commit();
432
433     return ( $id, 'User created' );
434 }
435
436 sub Delete {
437     my $self = shift;
438
439     return ( 0, $self->loc('Deleting this object would violate referential integrity') );
440
441 }
442
443 =head2 Load
444
445 Load a user object from the database. Takes a single argument.
446 If the argument is numerical, load by the column 'id'. If a user
447 object or its subclass passed then loads the same user by id.
448 Otherwise, load by the "Name" column which is the user's textual
449 username.
450
451 =cut
452
453 sub Load {
454     my $self = shift;
455     my $identifier = shift || return undef;
456
457     if ( $identifier !~ /\D/ ) {
458         return $self->SUPER::LoadById( $identifier );
459     } elsif ( UNIVERSAL::isa( $identifier, 'RT::User' ) ) {
460         return $self->SUPER::LoadById( $identifier->Id );
461     } else {
462         return $self->LoadByCol( "Name", $identifier );
463     }
464 }
465
466 =head2 LoadByEmail
467
468 Tries to load this user object from the database by the user's email address.
469
470 =cut
471
472 sub LoadByEmail {
473     my $self    = shift;
474     my $address = shift;
475
476     # Never load an empty address as an email address.
477     unless ($address) {
478         return (undef);
479     }
480
481     $address = $self->CanonicalizeEmailAddress($address);
482
483     #$RT::Logger->debug("Trying to load an email address: $address");
484     return $self->LoadByCol( "EmailAddress", $address );
485 }
486
487 =head2 LoadOrCreateByEmail ADDRESS
488
489 Attempts to find a user who has the provided email address. If that fails, creates an unprivileged user with
490 the provided email address and loads them. Address can be provided either as L<Email::Address> object
491 or string which is parsed using the module.
492
493 Returns a tuple of the user's id and a status message.
494 0 will be returned in place of the user's id in case of failure.
495
496 =cut
497
498 sub LoadOrCreateByEmail {
499     my $self = shift;
500     my $email = shift;
501
502     my ($message, $name);
503     if ( UNIVERSAL::isa( $email => 'Email::Address' ) ) {
504         ($email, $name) = ($email->address, $email->phrase);
505     } else {
506         ($email, $name) = RT::Interface::Email::ParseAddressFromHeader( $email );
507     }
508
509     $self->LoadByEmail( $email );
510     $self->Load( $email ) unless $self->Id;
511     $message = $self->loc('User loaded');
512
513     unless( $self->Id ) {
514         my $val;
515         ($val, $message) = $self->Create(
516             Name         => $email,
517             EmailAddress => $email,
518             RealName     => $name,
519             Privileged   => 0,
520             Comments     => 'Autocreated when added as a watcher',
521         );
522         unless ( $val ) {
523             # Deal with the race condition of two account creations at once
524             $self->LoadByEmail( $email );
525             unless ( $self->Id ) {
526                 sleep 5;
527                 $self->LoadByEmail( $email );
528             }
529             if ( $self->Id ) {
530                 $RT::Logger->error("Recovered from creation failure due to race condition");
531                 $message = $self->loc("User loaded");
532             } else {
533                 $RT::Logger->crit("Failed to create user ". $email .": " .$message);
534             }
535         }
536     }
537     return (0, $message) unless $self->id;
538     return ($self->Id, $message);
539 }
540
541 =head2 ValidateEmailAddress ADDRESS
542
543 Returns true if the email address entered is not in use by another user or is
544 undef or ''. Returns false if it's in use.
545
546 =cut
547
548 sub ValidateEmailAddress {
549     my $self  = shift;
550     my $Value = shift;
551
552     # if the email address is null, it's always valid
553     return (1) if ( !$Value || $Value eq "" );
554
555     if ( RT->Config->Get('ValidateUserEmailAddresses') ) {
556         # We only allow one valid email address
557         my @addresses = Email::Address->parse($Value);
558         return ( 0, $self->loc('Invalid syntax for email address') ) unless ( ( scalar (@addresses) == 1 ) && ( $addresses[0]->address ) );
559     }
560
561
562     my $TempUser = RT::User->new(RT->SystemUser);
563     $TempUser->LoadByEmail($Value);
564
565     if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) )
566     {    # if we found a user with that address
567             # it's invalid to set this user's address to it
568         return ( 0, $self->loc('Email address in use') );
569     } else {    #it's a valid email address
570         return (1);
571     }
572 }
573
574 =head2 SetEmailAddress
575
576 Check to make sure someone else isn't using this email address already
577 so that a better email address can be returned
578
579 =cut
580
581 sub SetEmailAddress {
582     my $self  = shift;
583     my $Value = shift;
584     $Value = '' unless defined $Value;
585
586     my ($val, $message) = $self->ValidateEmailAddress( $Value );
587     if ( $val ) {
588         return $self->_Set( Field => 'EmailAddress', Value => $Value );
589     } else {
590         return ( 0, $message )
591     }
592
593 }
594
595 =head2 EmailFrequency
596
597 Takes optional Ticket argument in paramhash. Returns 'no email',
598 'squelched', 'daily', 'weekly' or empty string depending on
599 user preferences.
600
601 =over 4
602
603 =item 'no email' - user has no email, so can not recieve notifications.
604
605 =item 'squelched' - returned only when Ticket argument is provided and
606 notifications to the user has been supressed for this ticket.
607
608 =item 'daily' - retruned when user recieve daily messages digest instead
609 of immediate delivery.
610
611 =item 'weekly' - previous, but weekly.
612
613 =item empty string returned otherwise.
614
615 =back
616
617 =cut
618
619 sub EmailFrequency {
620     my $self = shift;
621     my %args = (
622         Ticket => undef,
623         @_
624     );
625     return '' unless $self->id && $self->id != RT->Nobody->id
626         && $self->id != RT->SystemUser->id;
627     return 'no email address' unless my $email = $self->EmailAddress;
628     return 'email disabled for ticket' if $args{'Ticket'} &&
629         grep lc $email eq lc $_->Content, $args{'Ticket'}->SquelchMailTo;
630     my $frequency = RT->Config->Get( 'EmailFrequency', $self ) || '';
631     return 'daily' if $frequency =~ /daily/i;
632     return 'weekly' if $frequency =~ /weekly/i;
633     return '';
634 }
635
636 =head2 CanonicalizeEmailAddress ADDRESS
637
638 CanonicalizeEmailAddress converts email addresses into canonical form.
639 it takes one email address in and returns the proper canonical
640 form. You can dump whatever your proper local config is in here.  Note
641 that it may be called as a static method; in this case the first argument
642 is class name not an object.
643
644 =cut
645
646 sub CanonicalizeEmailAddress {
647     my $self = shift;
648     my $email = shift;
649     # Example: the following rule would treat all email
650     # coming from a subdomain as coming from second level domain
651     # foo.com
652     if ( my $match   = RT->Config->Get('CanonicalizeEmailAddressMatch') and
653          my $replace = RT->Config->Get('CanonicalizeEmailAddressReplace') )
654     {
655         $email =~ s/$match/$replace/gi;
656     }
657     return ($email);
658 }
659
660 =head2 CanonicalizeUserInfo HASH of ARGS
661
662 CanonicalizeUserInfo can convert all User->Create options.
663 it takes a hashref of all the params sent to User->Create and
664 returns that same hash, by default nothing is done.
665
666 This function is intended to allow users to have their info looked up via
667 an outside source and modified upon creation.
668
669 =cut
670
671 sub CanonicalizeUserInfo {
672     my $self = shift;
673     my $args = shift;
674     my $success = 1;
675
676     return ($success);
677 }
678
679
680 =head2 Password and authentication related functions
681
682 =head3 SetRandomPassword
683
684 Takes no arguments. Returns a status code and a new password or an error message.
685 If the status is 1, the second value returned is the new password.
686 If the status is anything else, the new value returned is the error code.
687
688 =cut
689
690 sub SetRandomPassword {
691     my $self = shift;
692
693     unless ( $self->CurrentUserCanModify('Password') ) {
694         return ( 0, $self->loc("Permission Denied") );
695     }
696
697
698     my $min = ( RT->Config->Get('MinimumPasswordLength') > 6 ?  RT->Config->Get('MinimumPasswordLength') : 6);
699     my $max = ( RT->Config->Get('MinimumPasswordLength') > 8 ?  RT->Config->Get('MinimumPasswordLength') : 8);
700
701     my $pass = $self->GenerateRandomPassword( $min, $max) ;
702
703     # If we have "notify user on
704
705     my ( $val, $msg ) = $self->SetPassword($pass);
706
707     #If we got an error return the error.
708     return ( 0, $msg ) unless ($val);
709
710     #Otherwise, we changed the password, lets return it.
711     return ( 1, $pass );
712
713 }
714
715 =head3 ResetPassword
716
717 Returns status, [ERROR or new password].  Resets this user's password to
718 a randomly generated pronouncable password and emails them, using a
719 global template called "PasswordChange".
720
721 This function is currently unused in the UI, but available for local scripts.
722
723 =cut
724
725 sub ResetPassword {
726     my $self = shift;
727
728     unless ( $self->CurrentUserCanModify('Password') ) {
729         return ( 0, $self->loc("Permission Denied") );
730     }
731     my ( $status, $pass ) = $self->SetRandomPassword();
732
733     unless ($status) {
734         return ( 0, "$pass" );
735     }
736
737     my $ret = RT::Interface::Email::SendEmailUsingTemplate(
738         To        => $self->EmailAddress,
739         Template  => 'PasswordChange',
740         Arguments => {
741             NewPassword => $pass,
742         },
743     );
744
745     if ($ret) {
746         return ( 1, $self->loc('New password notification sent') );
747     } else {
748         return ( 0, $self->loc('Notification could not be sent') );
749     }
750
751 }
752
753 =head3 GenerateRandomPassword MIN_LEN and MAX_LEN
754
755 Returns a random password between MIN_LEN and MAX_LEN characters long.
756
757 =cut
758
759 sub GenerateRandomPassword {
760     my $self = shift;   # just to drop it
761     return Text::Password::Pronounceable->generate(@_);
762 }
763
764 sub SafeSetPassword {
765     my $self = shift;
766     my %args = (
767         Current      => undef,
768         New          => undef,
769         Confirmation => undef,
770         @_,
771     );
772     return (1) unless defined $args{'New'} && length $args{'New'};
773
774     my %cond = $self->CurrentUserRequireToSetPassword;
775
776     unless ( $cond{'CanSet'} ) {
777         return (0, $self->loc('You can not set password.') .' '. $cond{'Reason'} );
778     }
779
780     my $error = '';
781     if ( $cond{'RequireCurrent'} && !$self->CurrentUser->IsPassword($args{'Current'}) ) {
782         if ( defined $args{'Current'} && length $args{'Current'} ) {
783             $error = $self->loc("Please enter your current password correctly.");
784         } else {
785             $error = $self->loc("Please enter your current password.");
786         }
787     } elsif ( $args{'New'} ne $args{'Confirmation'} ) {
788         $error = $self->loc("Passwords do not match.");
789     }
790
791     if ( $error ) {
792         $error .= ' '. $self->loc('Password has not been set.');
793         return (0, $error);
794     }
795
796     return $self->SetPassword( $args{'New'} );
797 }
798
799 =head3 SetPassword
800
801 Takes a string. Checks the string's length and sets this user's password
802 to that string.
803
804 =cut
805
806 sub SetPassword {
807     my $self     = shift;
808     my $password = shift;
809
810     unless ( $self->CurrentUserCanModify('Password') ) {
811         return ( 0, $self->loc('Password: Permission Denied') );
812     }
813
814     if ( !$password ) {
815         return ( 0, $self->loc("No password set") );
816     } else {
817         my ($val, $msg) = $self->ValidatePassword($password);
818         return ($val, $msg) if !$val;
819
820         my $new = !$self->HasPassword;
821         $password = $self->_GeneratePassword($password);
822
823         ( $val, $msg ) = $self->_Set(Field => 'Password', Value => $password);
824         if ($val) {
825             return ( 1, $self->loc("Password set") ) if $new;
826             return ( 1, $self->loc("Password changed") );
827         } else {
828             return ( $val, $msg );
829         }
830     }
831
832 }
833
834 sub _GeneratePassword_sha512 {
835     my $self = shift;
836     my ($password, $salt) = @_;
837
838     # Generate a 16-character base64 salt
839     unless ($salt) {
840         $salt = "";
841         $salt .= ("a".."z", "A".."Z","0".."9", "+", "/")[rand 64]
842             for 1..16;
843     }
844
845     my $sha = Digest::SHA->new(512);
846     $sha->add($salt);
847     $sha->add(encode_utf8($password));
848     return join("!", "", "sha512", $salt, $sha->b64digest);
849 }
850
851 =head3 _GeneratePassword PASSWORD [, SALT]
852
853 Returns a string to store in the database.  This string takes the form:
854
855    !method!salt!hash
856
857 By default, the method is currently C<sha512>.
858
859 =cut
860
861 sub _GeneratePassword {
862     my $self = shift;
863     return $self->_GeneratePassword_sha512(@_);
864 }
865
866 =head3 HasPassword
867
868 Returns true if the user has a valid password, otherwise returns false.
869
870 =cut
871
872 sub HasPassword {
873     my $self = shift;
874     my $pwd = $self->__Value('Password');
875     return undef if !defined $pwd
876                     || $pwd eq ''
877                     || $pwd eq '*NO-PASSWORD*';
878     return 1;
879 }
880
881 =head3 IsPassword
882
883 Returns true if the passed in value is this user's password.
884 Returns undef otherwise.
885
886 =cut
887
888 sub IsPassword {
889     my $self  = shift;
890     my $value = shift;
891
892     #TODO there isn't any apparent way to legitimately ACL this
893
894     # RT does not allow null passwords
895     if ( ( !defined($value) ) or ( $value eq '' ) ) {
896         return (undef);
897     }
898
899    if ( $self->PrincipalObj->Disabled ) {
900         $RT::Logger->info(
901             "Disabled user " . $self->Name . " tried to log in" );
902         return (undef);
903     }
904
905     unless ($self->HasPassword) {
906         return(undef);
907      }
908
909     my $stored = $self->__Value('Password');
910     if ($stored =~ /^!/) {
911         # If it's a new-style (>= RT 4.0) password, it starts with a '!'
912         my (undef, $method, $salt, undef) = split /!/, $stored;
913         if ($method eq "sha512") {
914             return $self->_GeneratePassword_sha512($value, $salt) eq $stored;
915         } else {
916             $RT::Logger->warn("Unknown hash method $method");
917             return 0;
918         }
919     } elsif (length $stored == 40) {
920         # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long
921         my $hash = MIME::Base64::decode_base64($stored);
922         # Decoding yields 30 byes; first 4 are the salt, the rest are substr(SHA256,0,26)
923         my $salt = substr($hash, 0, 4, "");
924         return 0 unless substr(Digest::SHA::sha256($salt . Digest::MD5::md5($value)), 0, 26) eq $hash;
925     } elsif (length $stored == 32) {
926         # Hex nonsalted-md5
927         return 0 unless Digest::MD5::md5_hex(encode_utf8($value)) eq $stored;
928     } elsif (length $stored == 22) {
929         # Base64 nonsalted-md5
930         return 0 unless Digest::MD5::md5_base64(encode_utf8($value)) eq $stored;
931     } elsif (length $stored == 13) {
932         # crypt() output
933         return 0 unless crypt(encode_utf8($value), $stored) eq $stored;
934     } else {
935         $RT::Logger->warn("Unknown password form");
936         return 0;
937     }
938
939     # We got here by validating successfully, but with a legacy
940     # password form.  Update to the most recent form.
941     my $obj = $self->isa("RT::CurrentUser") ? $self->UserObj : $self;
942     $obj->_Set(Field => 'Password', Value =>  $self->_GeneratePassword($value) );
943     return 1;
944 }
945
946 sub CurrentUserRequireToSetPassword {
947     my $self = shift;
948
949     my %res = (
950         CanSet => 1,
951         Reason => '',
952         RequireCurrent => 1,
953     );
954
955     if ( RT->Config->Get('WebExternalAuth')
956         && !RT->Config->Get('WebFallbackToInternalAuth')
957     ) {
958         $res{'CanSet'} = 0;
959         $res{'Reason'} = $self->loc("External authentication enabled.");
960     } elsif ( !$self->CurrentUser->HasPassword ) {
961         if ( $self->CurrentUser->id == ($self->id||0) ) {
962             # don't require current password if user has no
963             $res{'RequireCurrent'} = 0;
964         } else {
965             $res{'CanSet'} = 0;
966             $res{'Reason'} = $self->loc("Your password is not set.");
967         }
968     }
969
970     return %res;
971 }
972
973 =head3 AuthToken
974
975 Returns an authentication string associated with the user. This
976 string can be used to generate passwordless URLs to integrate
977 RT with services and programms like callendar managers, rss
978 readers and other.
979
980 =cut
981
982 sub AuthToken {
983     my $self = shift;
984     my $secret = $self->_Value( AuthToken => @_ );
985     return $secret if $secret;
986
987     $secret = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16);
988
989     my $tmp = RT::User->new( RT->SystemUser );
990     $tmp->Load( $self->id );
991     my ($status, $msg) = $tmp->SetAuthToken( $secret );
992     unless ( $status ) {
993         $RT::Logger->error( "Couldn't set auth token: $msg" );
994         return undef;
995     }
996     return $secret;
997 }
998
999 =head3 GenerateAuthToken
1000
1001 Generate a random authentication string for the user.
1002
1003 =cut
1004
1005 sub GenerateAuthToken {
1006     my $self = shift;
1007     my $token = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16);
1008     return $self->SetAuthToken( $token );
1009 }
1010
1011 =head3 GenerateAuthString
1012
1013 Takes a string and returns back a hex hash string. Later you can use
1014 this pair to make sure it's generated by this user using L</ValidateAuthString>
1015
1016 =cut
1017
1018 sub GenerateAuthString {
1019     my $self = shift;
1020     my $protect = shift;
1021
1022     my $str = $self->AuthToken . $protect;
1023     utf8::encode($str);
1024
1025     return substr(Digest::MD5::md5_hex($str),0,16);
1026 }
1027
1028 =head3 ValidateAuthString
1029
1030 Takes auth string and protected string. Returns true is protected string
1031 has been protected by user's L</AuthToken>. See also L</GenerateAuthString>.
1032
1033 =cut
1034
1035 sub ValidateAuthString {
1036     my $self = shift;
1037     my $auth_string = shift;
1038     my $protected = shift;
1039
1040     my $str = $self->AuthToken . $protected;
1041     utf8::encode( $str );
1042
1043     return $auth_string eq substr(Digest::MD5::md5_hex($str),0,16);
1044 }
1045
1046 =head2 SetDisabled
1047
1048 Toggles the user's disabled flag.
1049 If this flag is
1050 set, all password checks for this user will fail. All ACL checks for this
1051 user will fail. The user will appear in no user listings.
1052
1053 =cut
1054
1055 sub SetDisabled {
1056     my $self = shift;
1057     my $val = shift;
1058     unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
1059         return (0, $self->loc('Permission Denied'));
1060     }
1061
1062     $RT::Handle->BeginTransaction();
1063     my $set_err = $self->PrincipalObj->SetDisabled($val);
1064     unless ($set_err) {
1065         $RT::Handle->Rollback();
1066         $RT::Logger->warning(sprintf("Couldn't %s user %s", ($val == 1) ? "disable" : "enable", $self->PrincipalObj->Id));
1067         return (undef);
1068     }
1069     $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
1070
1071     $RT::Handle->Commit();
1072
1073     if ( $val == 1 ) {
1074         return (1, $self->loc("User disabled"));
1075     } else {
1076         return (1, $self->loc("User enabled"));
1077     }
1078
1079 }
1080
1081 =head2 Disabled
1082
1083 Returns true if user is disabled or false otherwise
1084
1085 =cut
1086
1087 sub Disabled {
1088     my $self = shift;
1089     return $self->PrincipalObj->Disabled(@_);
1090 }
1091
1092 =head2 PrincipalObj
1093
1094 Returns the principal object for this user. returns an empty RT::Principal
1095 if there's no principal object matching this user.
1096 The response is cached. PrincipalObj should never ever change.
1097
1098 =cut
1099
1100 sub PrincipalObj {
1101     my $self = shift;
1102
1103     unless ( $self->id ) {
1104         $RT::Logger->error("Couldn't get principal for an empty user");
1105         return undef;
1106     }
1107
1108     if ( !$self->{_principal_obj} ) {
1109
1110         my $obj = RT::Principal->new( $self->CurrentUser );
1111         $obj->LoadById( $self->id );
1112         if (! $obj->id ) {
1113             $RT::Logger->crit( 'No principal for user #' . $self->id );
1114             return undef;
1115         } elsif ( $obj->PrincipalType ne 'User' ) {
1116             $RT::Logger->crit(   'User #' . $self->id . ' has principal of ' . $obj->PrincipalType . ' type' );
1117             return undef;
1118         }
1119         $self->{_principal_obj} = $obj;
1120     }
1121     return $self->{_principal_obj};
1122 }
1123
1124
1125 =head2 PrincipalId
1126
1127 Returns this user's PrincipalId
1128
1129 =cut
1130
1131 sub PrincipalId {
1132     my $self = shift;
1133     return $self->Id;
1134 }
1135
1136 =head2 HasGroupRight
1137
1138 Takes a paramhash which can contain
1139 these items:
1140     GroupObj => RT::Group or Group => integer
1141     Right => 'Right'
1142
1143
1144 Returns 1 if this user has the right specified in the paramhash for the Group
1145 passed in.
1146
1147 Returns undef if they don't.
1148
1149 =cut
1150
1151 sub HasGroupRight {
1152     my $self = shift;
1153     my %args = (
1154         GroupObj    => undef,
1155         Group       => undef,
1156         Right       => undef,
1157         @_
1158     );
1159
1160
1161     if ( defined $args{'Group'} ) {
1162         $args{'GroupObj'} = RT::Group->new( $self->CurrentUser );
1163         $args{'GroupObj'}->Load( $args{'Group'} );
1164     }
1165
1166     # Validate and load up the GroupId
1167     unless ( ( defined $args{'GroupObj'} ) and ( $args{'GroupObj'}->Id ) ) {
1168         return undef;
1169     }
1170
1171     # Figure out whether a user has the right we're asking about.
1172     my $retval = $self->HasRight(
1173         Object => $args{'GroupObj'},
1174         Right     => $args{'Right'},
1175     );
1176
1177     return ($retval);
1178 }
1179
1180 =head2 OwnGroups
1181
1182 Returns a group collection object containing the groups of which this
1183 user is a member.
1184
1185 =cut
1186
1187 sub OwnGroups {
1188     my $self = shift;
1189     my $groups = RT::Groups->new($self->CurrentUser);
1190     $groups->LimitToUserDefinedGroups;
1191     $groups->WithMember(
1192         PrincipalId => $self->Id,
1193         Recursively => 1
1194     );
1195     return $groups;
1196 }
1197
1198 =head2 HasRight
1199
1200 Shim around PrincipalObj->HasRight. See L<RT::Principal>.
1201
1202 =cut
1203
1204 sub HasRight {
1205     my $self = shift;
1206     return $self->PrincipalObj->HasRight(@_);
1207 }
1208
1209 =head2 CurrentUserCanSee [FIELD]
1210
1211 Returns true if the current user can see the user, based on if it is
1212 public, ourself, or we have AdminUsers
1213
1214 =cut
1215
1216 sub CurrentUserCanSee {
1217     my $self = shift;
1218     my ($what) = @_;
1219
1220     # If it's public, fine.  Note that $what may be "transaction", which
1221     # doesn't have an Accessible value, and thus falls through below.
1222     if ( $self->_Accessible( $what, 'public' ) ) {
1223         return 1;
1224     }
1225
1226     # Users can see their own properties
1227     elsif ( defined($self->Id) && $self->CurrentUser->Id == $self->Id ) {
1228         return 1;
1229     }
1230
1231     # If the user has the admin users right, that's also enough
1232     elsif ( $self->CurrentUser->HasRight( Right => 'AdminUsers', Object => $RT::System) ) {
1233         return 1;
1234     }
1235     else {
1236         return 0;
1237     }
1238 }
1239
1240 =head2 CurrentUserCanModify RIGHT
1241
1242 If the user has rights for this object, either because
1243 he has 'AdminUsers' or (if he's trying to edit himself and the right isn't an
1244 admin right) 'ModifySelf', return 1. otherwise, return undef.
1245
1246 =cut
1247
1248 sub CurrentUserCanModify {
1249     my $self  = shift;
1250     my $field = shift;
1251
1252     if ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
1253         return (1);
1254     }
1255
1256     #If the field is marked as an "administrators only" field,
1257     # don't let the user touch it.
1258     elsif ( $self->_Accessible( $field, 'admin' ) ) {
1259         return (undef);
1260     }
1261
1262     #If the current user is trying to modify themselves
1263     elsif ( ( $self->id == $self->CurrentUser->id )
1264         and ( $self->CurrentUser->HasRight(Right => 'ModifySelf', Object => $RT::System) ) )
1265     {
1266         return (1);
1267     }
1268
1269     #If we don't have a good reason to grant them rights to modify
1270     # by now, they lose
1271     else {
1272         return (undef);
1273     }
1274
1275 }
1276
1277 =head2 CurrentUserHasRight
1278
1279 Takes a single argument. returns 1 if $Self->CurrentUser
1280 has the requested right. returns undef otherwise
1281
1282 =cut
1283
1284 sub CurrentUserHasRight {
1285     my $self  = shift;
1286     my $right = shift;
1287
1288     return ( $self->CurrentUser->HasRight(Right => $right, Object => $RT::System) );
1289 }
1290
1291 sub _PrefName {
1292     my $name = shift;
1293     if (ref $name) {
1294         $name = ref($name).'-'.$name->Id;
1295     }
1296
1297     return 'Pref-'.$name;
1298 }
1299
1300 =head2 Preferences NAME/OBJ DEFAULT
1301
1302 Obtain user preferences associated with given object or name.
1303 Returns DEFAULT if no preferences found.  If DEFAULT is a hashref,
1304 override the entries with user preferences.
1305
1306 =cut
1307
1308 sub Preferences {
1309     my $self  = shift;
1310     my $name = _PrefName (shift);
1311     my $default = shift;
1312
1313     my $attr = RT::Attribute->new( $self->CurrentUser );
1314     $attr->LoadByNameAndObject( Object => $self, Name => $name );
1315
1316     my $content = $attr->Id ? $attr->Content : undef;
1317     unless ( ref $content eq 'HASH' ) {
1318         return defined $content ? $content : $default;
1319     }
1320
1321     if (ref $default eq 'HASH') {
1322         for (keys %$default) {
1323             exists $content->{$_} or $content->{$_} = $default->{$_};
1324         }
1325     } elsif (defined $default) {
1326         $RT::Logger->error("Preferences $name for user".$self->Id." is hash but default is not");
1327     }
1328     return $content;
1329 }
1330
1331 =head2 SetPreferences NAME/OBJ VALUE
1332
1333 Set user preferences associated with given object or name.
1334
1335 =cut
1336
1337 sub SetPreferences {
1338     my $self = shift;
1339     my $name = _PrefName( shift );
1340     my $value = shift;
1341
1342     return (0, $self->loc("No permission to set preferences"))
1343         unless $self->CurrentUserCanModify('Preferences');
1344
1345     my $attr = RT::Attribute->new( $self->CurrentUser );
1346     $attr->LoadByNameAndObject( Object => $self, Name => $name );
1347     if ( $attr->Id ) {
1348         my ($ok, $msg) = $attr->SetContent( $value );
1349         return (1, "No updates made")
1350             if $msg eq "That is already the current value";
1351         return ($ok, $msg);
1352     } else {
1353         return $self->AddAttribute( Name => $name, Content => $value );
1354     }
1355 }
1356
1357 =head2 Stylesheet
1358
1359 Returns a list of valid stylesheets take from preferences.
1360
1361 =cut
1362
1363 sub Stylesheet {
1364     my $self = shift;
1365
1366     my $style = RT->Config->Get('WebDefaultStylesheet', $self->CurrentUser);
1367
1368     if (RT::Interface::Web->ComponentPathIsSafe($style)) {
1369         my @css_paths = map { $_ . '/NoAuth/css' } RT::Interface::Web->ComponentRoots;
1370
1371         for my $css_path (@css_paths) {
1372             if (-d "$css_path/$style") {
1373                 return $style
1374             }
1375         }
1376     }
1377
1378     # Fall back to the system stylesheet.
1379     return RT->Config->Get('WebDefaultStylesheet');
1380 }
1381
1382 =head2 WatchedQueues ROLE_LIST
1383
1384 Returns a RT::Queues object containing every queue watched by the user.
1385
1386 Takes a list of roles which is some subset of ('Cc', 'AdminCc').  Defaults to:
1387
1388 $user->WatchedQueues('Cc', 'AdminCc');
1389
1390 =cut
1391
1392 sub WatchedQueues {
1393
1394     my $self = shift;
1395     my @roles = @_ || ('Cc', 'AdminCc');
1396
1397     $RT::Logger->debug('WatcheQueues got user ' . $self->Name);
1398
1399     my $watched_queues = RT::Queues->new($self->CurrentUser);
1400
1401     my $group_alias = $watched_queues->Join(
1402                                              ALIAS1 => 'main',
1403                                              FIELD1 => 'id',
1404                                              TABLE2 => 'Groups',
1405                                              FIELD2 => 'Instance',
1406                                            );
1407
1408     $watched_queues->Limit(
1409                             ALIAS => $group_alias,
1410                             FIELD => 'Domain',
1411                             VALUE => 'RT::Queue-Role',
1412                             ENTRYAGGREGATOR => 'AND',
1413                           );
1414     if (grep { $_ eq 'Cc' } @roles) {
1415         $watched_queues->Limit(
1416                                 SUBCLAUSE => 'LimitToWatchers',
1417                                 ALIAS => $group_alias,
1418                                 FIELD => 'Type',
1419                                 VALUE => 'Cc',
1420                                 ENTRYAGGREGATOR => 'OR',
1421                               );
1422     }
1423     if (grep { $_ eq 'AdminCc' } @roles) {
1424         $watched_queues->Limit(
1425                                 SUBCLAUSE => 'LimitToWatchers',
1426                                 ALIAS => $group_alias,
1427                                 FIELD => 'Type',
1428                                 VALUE => 'AdminCc',
1429                                 ENTRYAGGREGATOR => 'OR',
1430                               );
1431     }
1432
1433     my $queues_alias = $watched_queues->Join(
1434                                               ALIAS1 => $group_alias,
1435                                               FIELD1 => 'id',
1436                                               TABLE2 => 'CachedGroupMembers',
1437                                               FIELD2 => 'GroupId',
1438                                             );
1439     $watched_queues->Limit(
1440                             ALIAS => $queues_alias,
1441                             FIELD => 'MemberId',
1442                             VALUE => $self->PrincipalId,
1443                           );
1444     $watched_queues->Limit(
1445                             ALIAS => $queues_alias,
1446                             FIELD => 'Disabled',
1447                             VALUE => 0,
1448                           );
1449
1450
1451     $RT::Logger->debug("WatchedQueues got " . $watched_queues->Count . " queues");
1452
1453     return $watched_queues;
1454
1455 }
1456
1457 sub _Set {
1458     my $self = shift;
1459
1460     my %args = (
1461         Field => undef,
1462         Value => undef,
1463     TransactionType   => 'Set',
1464     RecordTransaction => 1,
1465         @_
1466     );
1467
1468     # Nobody is allowed to futz with RT_System or Nobody
1469
1470     if ( ($self->Id == RT->SystemUser->Id )  ||
1471          ($self->Id == RT->Nobody->Id)) {
1472         return ( 0, $self->loc("Can not modify system users") );
1473     }
1474     unless ( $self->CurrentUserCanModify( $args{'Field'} ) ) {
1475         return ( 0, $self->loc("Permission Denied") );
1476     }
1477
1478     my $Old = $self->SUPER::_Value("$args{'Field'}");
1479
1480     my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1481                       Value => $args{'Value'} );
1482
1483     #If we can't actually set the field to the value, don't record
1484     # a transaction. instead, get out of here.
1485     if ( $ret == 0 ) { return ( 0, $msg ); }
1486
1487     if ( $args{'RecordTransaction'} == 1 ) {
1488         if ($args{'Field'} eq "Password") {
1489             $args{'Value'} = $Old = '********';
1490         }
1491         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1492                                                Type => $args{'TransactionType'},
1493                                                Field     => $args{'Field'},
1494                                                NewValue  => $args{'Value'},
1495                                                OldValue  => $Old,
1496                                                TimeTaken => $args{'TimeTaken'},
1497         );
1498         return ( $Trans, scalar $TransObj->BriefDescription );
1499     } else {
1500         return ( $ret, $msg );
1501     }
1502 }
1503
1504 =head2 _Value
1505
1506 Takes the name of a table column.
1507 Returns its value as a string, if the user passes an ACL check
1508
1509 =cut
1510
1511 sub _Value {
1512
1513     my $self  = shift;
1514     my $field = shift;
1515
1516     # Defer to the abstraction above to know if the field can be read
1517     return $self->SUPER::_Value($field) if $self->CurrentUserCanSee($field);
1518     return undef;
1519 }
1520
1521 =head2 FriendlyName
1522
1523 Return the friendly name
1524
1525 =cut
1526
1527 sub FriendlyName {
1528     my $self = shift;
1529     return $self->RealName if defined($self->RealName);
1530     return $self->Name if defined($self->Name);
1531     return "";
1532 }
1533
1534 =head2 PreferredKey
1535
1536 Returns the preferred key of the user. If none is set, then this will query
1537 GPG and set the preferred key to the maximally trusted key found (and then
1538 return it). Returns C<undef> if no preferred key can be found.
1539
1540 =cut
1541
1542 sub PreferredKey
1543 {
1544     my $self = shift;
1545     return undef unless RT->Config->Get('GnuPG')->{'Enable'};
1546
1547     if ( ($self->CurrentUser->Id != $self->Id )  &&
1548           !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) {
1549           return undef;
1550     }
1551
1552
1553
1554     my $prefkey = $self->FirstAttribute('PreferredKey');
1555     return $prefkey->Content if $prefkey;
1556
1557     # we don't have a preferred key for this user, so now we must query GPG
1558     require RT::Crypt::GnuPG;
1559     my %res = RT::Crypt::GnuPG::GetKeysForEncryption($self->EmailAddress);
1560     return undef unless defined $res{'info'};
1561     my @keys = @{ $res{'info'} };
1562     return undef if @keys == 0;
1563
1564     if (@keys == 1) {
1565         $prefkey = $keys[0]->{'Fingerprint'};
1566     } else {
1567         # prefer the maximally trusted key
1568         @keys = sort { $b->{'TrustLevel'} <=> $a->{'TrustLevel'} } @keys;
1569         $prefkey = $keys[0]->{'Fingerprint'};
1570     }
1571
1572     $self->SetAttribute(Name => 'PreferredKey', Content => $prefkey);
1573     return $prefkey;
1574 }
1575
1576 sub PrivateKey {
1577     my $self = shift;
1578
1579
1580     #If the user wants to see their own values, let them.
1581     #If the user is an admin, let them.
1582     #Otherwwise, don't let them.
1583     #
1584     if ( ($self->CurrentUser->Id != $self->Id )  &&
1585           !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) {
1586           return undef;
1587     }
1588
1589     my $key = $self->FirstAttribute('PrivateKey') or return undef;
1590     return $key->Content;
1591 }
1592
1593 sub SetPrivateKey {
1594     my $self = shift;
1595     my $key = shift;
1596
1597     unless ($self->CurrentUserCanModify('PrivateKey')) {
1598         return (0, $self->loc("Permission Denied"));
1599     }
1600
1601     unless ( $key ) {
1602         my ($status, $msg) = $self->DeleteAttribute('PrivateKey');
1603         unless ( $status ) {
1604             $RT::Logger->error( "Couldn't delete attribute: $msg" );
1605             return ($status, $self->loc("Couldn't unset private key"));
1606         }
1607         return ($status, $self->loc("Unset private key"));
1608     }
1609
1610     # check that it's really private key
1611     {
1612         my %tmp = RT::Crypt::GnuPG::GetKeysForSigning( $key );
1613         return (0, $self->loc("No such key or it's not suitable for signing"))
1614             if $tmp{'exit_code'} || !$tmp{'info'};
1615     }
1616
1617     my ($status, $msg) = $self->SetAttribute(
1618         Name => 'PrivateKey',
1619         Content => $key,
1620     );
1621     return ($status, $self->loc("Couldn't set private key"))
1622         unless $status;
1623     return ($status, $self->loc("Set private key"));
1624 }
1625
1626 sub BasicColumns {
1627     (
1628     [ Name => 'Username' ],
1629     [ EmailAddress => 'Email' ],
1630     [ RealName => 'Name' ],
1631     [ Organization => 'Organization' ],
1632     );
1633 }
1634
1635 =head2 Create PARAMHASH
1636
1637 Create takes a hash of values and creates a row in the database:
1638
1639   varchar(200) 'Name'.
1640   varbinary(256) 'Password'.
1641   varchar(16) 'AuthToken'.
1642   text 'Comments'.
1643   text 'Signature'.
1644   varchar(120) 'EmailAddress'.
1645   text 'FreeformContactInfo'.
1646   varchar(200) 'Organization'.
1647   varchar(120) 'RealName'.
1648   varchar(16) 'NickName'.
1649   varchar(16) 'Lang'.
1650   varchar(16) 'EmailEncoding'.
1651   varchar(16) 'WebEncoding'.
1652   varchar(100) 'ExternalContactInfoId'.
1653   varchar(30) 'ContactInfoSystem'.
1654   varchar(100) 'ExternalAuthId'.
1655   varchar(30) 'AuthSystem'.
1656   varchar(16) 'Gecos'.
1657   varchar(30) 'HomePhone'.
1658   varchar(30) 'WorkPhone'.
1659   varchar(30) 'MobilePhone'.
1660   varchar(30) 'PagerPhone'.
1661   varchar(200) 'Address1'.
1662   varchar(200) 'Address2'.
1663   varchar(100) 'City'.
1664   varchar(100) 'State'.
1665   varchar(16) 'Zip'.
1666   varchar(50) 'Country'.
1667   varchar(50) 'Timezone'.
1668   text 'PGPKey'.
1669
1670 =cut
1671
1672
1673
1674
1675 =head2 id
1676
1677 Returns the current value of id. 
1678 (In the database, id is stored as int(11).)
1679
1680
1681 =cut
1682
1683
1684 =head2 Name
1685
1686 Returns the current value of Name. 
1687 (In the database, Name is stored as varchar(200).)
1688
1689
1690
1691 =head2 SetName VALUE
1692
1693
1694 Set Name to VALUE. 
1695 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1696 (In the database, Name will be stored as a varchar(200).)
1697
1698
1699 =cut
1700
1701
1702 =head2 Password
1703
1704 Returns the current value of Password. 
1705 (In the database, Password is stored as varchar(256).)
1706
1707
1708
1709 =head2 SetPassword VALUE
1710
1711
1712 Set Password to VALUE. 
1713 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1714 (In the database, Password will be stored as a varchar(256).)
1715
1716
1717 =cut
1718
1719
1720 =head2 AuthToken
1721
1722 Returns the current value of AuthToken. 
1723 (In the database, AuthToken is stored as varchar(16).)
1724
1725
1726
1727 =head2 SetAuthToken VALUE
1728
1729
1730 Set AuthToken to VALUE. 
1731 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1732 (In the database, AuthToken will be stored as a varchar(16).)
1733
1734
1735 =cut
1736
1737
1738 =head2 Comments
1739
1740 Returns the current value of Comments. 
1741 (In the database, Comments is stored as text.)
1742
1743
1744
1745 =head2 SetComments VALUE
1746
1747
1748 Set Comments to VALUE. 
1749 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1750 (In the database, Comments will be stored as a text.)
1751
1752
1753 =cut
1754
1755
1756 =head2 Signature
1757
1758 Returns the current value of Signature. 
1759 (In the database, Signature is stored as text.)
1760
1761
1762
1763 =head2 SetSignature VALUE
1764
1765
1766 Set Signature to VALUE. 
1767 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1768 (In the database, Signature will be stored as a text.)
1769
1770
1771 =cut
1772
1773
1774 =head2 EmailAddress
1775
1776 Returns the current value of EmailAddress. 
1777 (In the database, EmailAddress is stored as varchar(120).)
1778
1779
1780
1781 =head2 SetEmailAddress VALUE
1782
1783
1784 Set EmailAddress to VALUE. 
1785 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1786 (In the database, EmailAddress will be stored as a varchar(120).)
1787
1788
1789 =cut
1790
1791
1792 =head2 FreeformContactInfo
1793
1794 Returns the current value of FreeformContactInfo. 
1795 (In the database, FreeformContactInfo is stored as text.)
1796
1797
1798
1799 =head2 SetFreeformContactInfo VALUE
1800
1801
1802 Set FreeformContactInfo to VALUE. 
1803 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1804 (In the database, FreeformContactInfo will be stored as a text.)
1805
1806
1807 =cut
1808
1809
1810 =head2 Organization
1811
1812 Returns the current value of Organization. 
1813 (In the database, Organization is stored as varchar(200).)
1814
1815
1816
1817 =head2 SetOrganization VALUE
1818
1819
1820 Set Organization to VALUE. 
1821 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1822 (In the database, Organization will be stored as a varchar(200).)
1823
1824
1825 =cut
1826
1827
1828 =head2 RealName
1829
1830 Returns the current value of RealName. 
1831 (In the database, RealName is stored as varchar(120).)
1832
1833
1834
1835 =head2 SetRealName VALUE
1836
1837
1838 Set RealName to VALUE. 
1839 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1840 (In the database, RealName will be stored as a varchar(120).)
1841
1842
1843 =cut
1844
1845
1846 =head2 NickName
1847
1848 Returns the current value of NickName. 
1849 (In the database, NickName is stored as varchar(16).)
1850
1851
1852
1853 =head2 SetNickName VALUE
1854
1855
1856 Set NickName to VALUE. 
1857 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1858 (In the database, NickName will be stored as a varchar(16).)
1859
1860
1861 =cut
1862
1863
1864 =head2 Lang
1865
1866 Returns the current value of Lang. 
1867 (In the database, Lang is stored as varchar(16).)
1868
1869
1870
1871 =head2 SetLang VALUE
1872
1873
1874 Set Lang to VALUE. 
1875 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1876 (In the database, Lang will be stored as a varchar(16).)
1877
1878
1879 =cut
1880
1881
1882 =head2 EmailEncoding
1883
1884 Returns the current value of EmailEncoding. 
1885 (In the database, EmailEncoding is stored as varchar(16).)
1886
1887
1888
1889 =head2 SetEmailEncoding VALUE
1890
1891
1892 Set EmailEncoding to VALUE. 
1893 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1894 (In the database, EmailEncoding will be stored as a varchar(16).)
1895
1896
1897 =cut
1898
1899
1900 =head2 WebEncoding
1901
1902 Returns the current value of WebEncoding. 
1903 (In the database, WebEncoding is stored as varchar(16).)
1904
1905
1906
1907 =head2 SetWebEncoding VALUE
1908
1909
1910 Set WebEncoding to VALUE. 
1911 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1912 (In the database, WebEncoding will be stored as a varchar(16).)
1913
1914
1915 =cut
1916
1917
1918 =head2 ExternalContactInfoId
1919
1920 Returns the current value of ExternalContactInfoId. 
1921 (In the database, ExternalContactInfoId is stored as varchar(100).)
1922
1923
1924
1925 =head2 SetExternalContactInfoId VALUE
1926
1927
1928 Set ExternalContactInfoId to VALUE. 
1929 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1930 (In the database, ExternalContactInfoId will be stored as a varchar(100).)
1931
1932
1933 =cut
1934
1935
1936 =head2 ContactInfoSystem
1937
1938 Returns the current value of ContactInfoSystem. 
1939 (In the database, ContactInfoSystem is stored as varchar(30).)
1940
1941
1942
1943 =head2 SetContactInfoSystem VALUE
1944
1945
1946 Set ContactInfoSystem to VALUE. 
1947 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1948 (In the database, ContactInfoSystem will be stored as a varchar(30).)
1949
1950
1951 =cut
1952
1953
1954 =head2 ExternalAuthId
1955
1956 Returns the current value of ExternalAuthId. 
1957 (In the database, ExternalAuthId is stored as varchar(100).)
1958
1959
1960
1961 =head2 SetExternalAuthId VALUE
1962
1963
1964 Set ExternalAuthId to VALUE. 
1965 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1966 (In the database, ExternalAuthId will be stored as a varchar(100).)
1967
1968
1969 =cut
1970
1971
1972 =head2 AuthSystem
1973
1974 Returns the current value of AuthSystem. 
1975 (In the database, AuthSystem is stored as varchar(30).)
1976
1977
1978
1979 =head2 SetAuthSystem VALUE
1980
1981
1982 Set AuthSystem to VALUE. 
1983 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1984 (In the database, AuthSystem will be stored as a varchar(30).)
1985
1986
1987 =cut
1988
1989
1990 =head2 Gecos
1991
1992 Returns the current value of Gecos. 
1993 (In the database, Gecos is stored as varchar(16).)
1994
1995
1996
1997 =head2 SetGecos VALUE
1998
1999
2000 Set Gecos to VALUE. 
2001 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2002 (In the database, Gecos will be stored as a varchar(16).)
2003
2004
2005 =cut
2006
2007
2008 =head2 HomePhone
2009
2010 Returns the current value of HomePhone. 
2011 (In the database, HomePhone is stored as varchar(30).)
2012
2013
2014
2015 =head2 SetHomePhone VALUE
2016
2017
2018 Set HomePhone to VALUE. 
2019 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2020 (In the database, HomePhone will be stored as a varchar(30).)
2021
2022
2023 =cut
2024
2025
2026 =head2 WorkPhone
2027
2028 Returns the current value of WorkPhone. 
2029 (In the database, WorkPhone is stored as varchar(30).)
2030
2031
2032
2033 =head2 SetWorkPhone VALUE
2034
2035
2036 Set WorkPhone to VALUE. 
2037 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2038 (In the database, WorkPhone will be stored as a varchar(30).)
2039
2040
2041 =cut
2042
2043
2044 =head2 MobilePhone
2045
2046 Returns the current value of MobilePhone. 
2047 (In the database, MobilePhone is stored as varchar(30).)
2048
2049
2050
2051 =head2 SetMobilePhone VALUE
2052
2053
2054 Set MobilePhone to VALUE. 
2055 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2056 (In the database, MobilePhone will be stored as a varchar(30).)
2057
2058
2059 =cut
2060
2061
2062 =head2 PagerPhone
2063
2064 Returns the current value of PagerPhone. 
2065 (In the database, PagerPhone is stored as varchar(30).)
2066
2067
2068
2069 =head2 SetPagerPhone VALUE
2070
2071
2072 Set PagerPhone to VALUE. 
2073 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2074 (In the database, PagerPhone will be stored as a varchar(30).)
2075
2076
2077 =cut
2078
2079
2080 =head2 Address1
2081
2082 Returns the current value of Address1. 
2083 (In the database, Address1 is stored as varchar(200).)
2084
2085
2086
2087 =head2 SetAddress1 VALUE
2088
2089
2090 Set Address1 to VALUE. 
2091 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2092 (In the database, Address1 will be stored as a varchar(200).)
2093
2094
2095 =cut
2096
2097
2098 =head2 Address2
2099
2100 Returns the current value of Address2. 
2101 (In the database, Address2 is stored as varchar(200).)
2102
2103
2104
2105 =head2 SetAddress2 VALUE
2106
2107
2108 Set Address2 to VALUE. 
2109 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2110 (In the database, Address2 will be stored as a varchar(200).)
2111
2112
2113 =cut
2114
2115
2116 =head2 City
2117
2118 Returns the current value of City. 
2119 (In the database, City is stored as varchar(100).)
2120
2121
2122
2123 =head2 SetCity VALUE
2124
2125
2126 Set City to VALUE. 
2127 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2128 (In the database, City will be stored as a varchar(100).)
2129
2130
2131 =cut
2132
2133
2134 =head2 State
2135
2136 Returns the current value of State. 
2137 (In the database, State is stored as varchar(100).)
2138
2139
2140
2141 =head2 SetState VALUE
2142
2143
2144 Set State to VALUE. 
2145 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2146 (In the database, State will be stored as a varchar(100).)
2147
2148
2149 =cut
2150
2151
2152 =head2 Zip
2153
2154 Returns the current value of Zip. 
2155 (In the database, Zip is stored as varchar(16).)
2156
2157
2158
2159 =head2 SetZip VALUE
2160
2161
2162 Set Zip to VALUE. 
2163 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2164 (In the database, Zip will be stored as a varchar(16).)
2165
2166
2167 =cut
2168
2169
2170 =head2 Country
2171
2172 Returns the current value of Country. 
2173 (In the database, Country is stored as varchar(50).)
2174
2175
2176
2177 =head2 SetCountry VALUE
2178
2179
2180 Set Country to VALUE. 
2181 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2182 (In the database, Country will be stored as a varchar(50).)
2183
2184
2185 =cut
2186
2187
2188 =head2 Timezone
2189
2190 Returns the current value of Timezone. 
2191 (In the database, Timezone is stored as varchar(50).)
2192
2193
2194
2195 =head2 SetTimezone VALUE
2196
2197
2198 Set Timezone to VALUE. 
2199 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2200 (In the database, Timezone will be stored as a varchar(50).)
2201
2202
2203 =cut
2204
2205
2206 =head2 PGPKey
2207
2208 Returns the current value of PGPKey. 
2209 (In the database, PGPKey is stored as text.)
2210
2211
2212
2213 =head2 SetPGPKey VALUE
2214
2215
2216 Set PGPKey to VALUE. 
2217 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2218 (In the database, PGPKey will be stored as a text.)
2219
2220
2221 =cut
2222
2223
2224 =head2 Creator
2225
2226 Returns the current value of Creator. 
2227 (In the database, Creator is stored as int(11).)
2228
2229
2230 =cut
2231
2232
2233 =head2 Created
2234
2235 Returns the current value of Created. 
2236 (In the database, Created is stored as datetime.)
2237
2238
2239 =cut
2240
2241
2242 =head2 LastUpdatedBy
2243
2244 Returns the current value of LastUpdatedBy. 
2245 (In the database, LastUpdatedBy is stored as int(11).)
2246
2247
2248 =cut
2249
2250
2251 =head2 LastUpdated
2252
2253 Returns the current value of LastUpdated. 
2254 (In the database, LastUpdated is stored as datetime.)
2255
2256
2257 =cut
2258
2259
2260
2261 sub _CoreAccessible {
2262     {
2263      
2264         id =>
2265         {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2266         Name => 
2267         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2268         Password => 
2269         {read => 1, write => 1, sql_type => 12, length => 256,  is_blob => 0,  is_numeric => 0,  type => 'varchar(256)', default => ''},
2270         AuthToken => 
2271         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2272         Comments => 
2273         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2274         Signature => 
2275         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2276         EmailAddress => 
2277         {read => 1, write => 1, sql_type => 12, length => 120,  is_blob => 0,  is_numeric => 0,  type => 'varchar(120)', default => ''},
2278         FreeformContactInfo => 
2279         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2280         Organization => 
2281         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2282         RealName => 
2283         {read => 1, write => 1, sql_type => 12, length => 120,  is_blob => 0,  is_numeric => 0,  type => 'varchar(120)', default => ''},
2284         NickName => 
2285         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2286         Lang => 
2287         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2288         EmailEncoding => 
2289         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2290         WebEncoding => 
2291         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2292         ExternalContactInfoId => 
2293         {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
2294         ContactInfoSystem => 
2295         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2296         ExternalAuthId => 
2297         {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
2298         AuthSystem => 
2299         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2300         Gecos => 
2301         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2302         HomePhone => 
2303         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2304         WorkPhone => 
2305         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2306         MobilePhone => 
2307         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2308         PagerPhone => 
2309         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2310         Address1 => 
2311         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2312         Address2 => 
2313         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2314         City => 
2315         {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
2316         State => 
2317         {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
2318         Zip => 
2319         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2320         Country => 
2321         {read => 1, write => 1, sql_type => 12, length => 50,  is_blob => 0,  is_numeric => 0,  type => 'varchar(50)', default => ''},
2322         Timezone => 
2323         {read => 1, write => 1, sql_type => 12, length => 50,  is_blob => 0,  is_numeric => 0,  type => 'varchar(50)', default => ''},
2324         PGPKey => 
2325         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2326         Creator => 
2327         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2328         Created => 
2329         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2330         LastUpdatedBy => 
2331         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2332         LastUpdated => 
2333         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2334
2335  }
2336 };
2337
2338 RT::Base->_ImportOverlays();
2339
2340
2341 1;