X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FUser_Overlay.pm;h=0f28568163fe7fa9df1c4cb6478a156a5277478a;hp=ba322cd4b4b93e9d7ef38d11344fcf72e5bde449;hb=666bacbcbeab98606a59a9a540c8e247f6203331;hpb=289340780927b5bac2c7604d7317c3063c6dd8cc diff --git a/rt/lib/RT/User_Overlay.pm b/rt/lib/RT/User_Overlay.pm index ba322cd4b..0f2856816 100644 --- a/rt/lib/RT/User_Overlay.pm +++ b/rt/lib/RT/User_Overlay.pm @@ -1,8 +1,14 @@ -# BEGIN LICENSE BLOCK +# BEGIN BPS TAGGED BLOCK {{{ # -# Copyright (c) 1996-2003 Jesse Vincent +# COPYRIGHT: # -# (Except where explictly superceded by other copyright notices) +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: # # 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 @@ -14,13 +20,32 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # -# Unless otherwise specified, all modifications, corrections or -# extensions to this work which alter its source code become the -# property of Best Practical Solutions, LLC when submitted for -# inclusion in the work. +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# # +# CONTRIBUTION SUBMISSION POLICY: # -# END LICENSE BLOCK +# (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.) +# +# 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. +# +# END BPS TAGGED BLOCK }}} + =head1 NAME RT::User - RT User object @@ -34,162 +59,49 @@ =head1 METHODS -=begin testing -ok(require RT::User); -=end testing +=cut -=cut +package RT::User; use strict; no warnings qw(redefine); -use vars qw(%_USERS_KEY_CACHE); - -%_USERS_KEY_CACHE = (); - use Digest::MD5; use RT::Principals; use RT::ACE; +use RT::Interface::Email; +use Encode; - -# {{{ sub _Accessible - - -sub _ClassAccessible { +sub _OverlayAccessible { { - - id => - {read => 1, type => 'int(11)', default => ''}, - Name => - {read => 1, write => 1, public => 1, admin => 1, type => 'varchar(120)', default => ''}, - Password => - { write => 1, type => 'varchar(40)', default => ''}, - Comments => - {read => 1, write => 1, admin => 1, type => 'blob', default => ''}, - Signature => - {read => 1, write => 1, type => 'blob', default => ''}, - EmailAddress => - {read => 1, write => 1, public => 1, type => 'varchar(120)', default => ''}, - FreeformContactInfo => - {read => 1, write => 1, type => 'blob', default => ''}, - Organization => - {read => 1, write => 1, public => 1, admin => 1, type => 'varchar(200)', default => ''}, - RealName => - {read => 1, write => 1, public => 1, type => 'varchar(120)', default => ''}, - NickName => - {read => 1, write => 1, public => 1, type => 'varchar(16)', default => ''}, - Lang => - {read => 1, write => 1, public => 1, type => 'varchar(16)', default => ''}, - EmailEncoding => - {read => 1, write => 1, public => 1, type => 'varchar(16)', default => ''}, - WebEncoding => - {read => 1, write => 1, public => 1, type => 'varchar(16)', default => ''}, - ExternalContactInfoId => - {read => 1, write => 1, public => 1, admin => 1, type => 'varchar(100)', default => ''}, - ContactInfoSystem => - {read => 1, write => 1, public => 1, admin => 1, type => 'varchar(30)', default => ''}, - ExternalAuthId => - {read => 1, write => 1, public => 1, admin => 1, type => 'varchar(100)', default => ''}, - AuthSystem => - {read => 1, write => 1, public => 1, admin => 1,type => 'varchar(30)', default => ''}, - Gecos => - {read => 1, write => 1, public => 1, admin => 1, type => 'varchar(16)', default => ''}, - - PGPKey => { - {read => 1, write => 1, public => 1, admin => 1, type => 'text', default => ''}, - }, - HomePhone => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, - WorkPhone => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, - MobilePhone => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, - PagerPhone => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, - Address1 => - {read => 1, write => 1, type => 'varchar(200)', default => ''}, - Address2 => - {read => 1, write => 1, type => 'varchar(200)', default => ''}, - City => - {read => 1, write => 1, type => 'varchar(100)', default => ''}, - State => - {read => 1, write => 1, type => 'varchar(100)', default => ''}, - Zip => - {read => 1, write => 1, type => 'varchar(16)', default => ''}, - Country => - {read => 1, write => 1, type => 'varchar(50)', default => ''}, - Creator => - {read => 1, auto => 1, type => 'int(11)', default => ''}, - Created => - {read => 1, auto => 1, type => 'datetime', default => ''}, - LastUpdatedBy => - {read => 1, auto => 1, type => 'int(11)', default => ''}, - LastUpdated => - {read => 1, auto => 1, type => 'datetime', default => ''}, - - } -}; + Name => { public => 1, admin => 1 }, + Password => { read => 0 }, + EmailAddress => { public => 1 }, + Organization => { public => 1, admin => 1 }, + RealName => { public => 1 }, + NickName => { public => 1 }, + Lang => { public => 1 }, + 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 }, + Gecos => { public => 1, admin => 1 }, + PGPKey => { public => 1, admin => 1 }, -# }}} - -# {{{ sub Create - -=head2 Create { PARAMHASH } - - -=begin testing - -# Make sure we can create a user - -my $u1 = RT::User->new($RT::SystemUser); -is(ref($u1), 'RT::User'); -my ($id, $msg) = $u1->Create(Name => 'CreateTest1', EmailAddress => 'create-test-1@example.com'); -ok ($id, "Creating user CreateTest1 - " . $msg ); - -# Make sure we can't create a second user with the same name -my $u2 = RT::User->new($RT::SystemUser); -($id, $msg) = $u2->Create(Name => 'CreateTest1', EmailAddress => 'create-test-2@example.com'); -ok (!$id, $msg); - - -# Make sure we can't create a second user with the same EmailAddress address -my $u3 = RT::User->new($RT::SystemUser); -($id, $msg) = $u3->Create(Name => 'CreateTest2', EmailAddress => 'create-test-1@example.com'); -ok (!$id, $msg); - -# Make sure we can create a user with no EmailAddress address -my $u4 = RT::User->new($RT::SystemUser); -($id, $msg) = $u4->Create(Name => 'CreateTest3'); -ok ($id, $msg); + } +} -# make sure we can create a second user with no EmailAddress address -my $u5 = RT::User->new($RT::SystemUser); -($id, $msg) = $u5->Create(Name => 'CreateTest4'); -ok ($id, $msg); -# make sure we can create a user with a blank EmailAddress address -my $u6 = RT::User->new($RT::SystemUser); -($id, $msg) = $u6->Create(Name => 'CreateTest6', EmailAddress => ''); -ok ($id, $msg); -# make sure we can create a second user with a blankEmailAddress address -my $u7 = RT::User->new($RT::SystemUser); -($id, $msg) = $u7->Create(Name => 'CreateTest7', EmailAddress => ''); -ok ($id, $msg); -# Can we change the email address away from from ""; -($id,$msg) = $u7->SetEmailAddress('foo@bar'); -ok ($id, $msg); -# can we change the address back to ""; -($id,$msg) = $u7->SetEmailAddress(''); -ok ($id, $msg); -is ($u7->EmailAddress, ''); +=head2 Create { PARAMHASH } -=end testing =cut @@ -200,23 +112,31 @@ sub Create { Privileged => 0, Disabled => 0, EmailAddress => '', + _RecordTransaction => 1, @_ # get the real argumentlist ); + # remove the value so it does not cripple SUPER::Create + my $record_transaction = delete $args{'_RecordTransaction'}; + #Check the ACL unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) { - return ( 0, $self->loc('No permission to create users') ); + return ( 0, $self->loc('Permission Denied') ); + } + + + unless ($self->CanonicalizeUserInfo(\%args)) { + return ( 0, $self->loc("Could not set user info") ); } $args{'EmailAddress'} = $self->CanonicalizeEmailAddress($args{'EmailAddress'}); + # if the user doesn't have a name defined, set it to the email address $args{'Name'} = $args{'EmailAddress'} unless ($args{'Name'}); - # Privileged is no longer a column in users - my $privileged = $args{'Privileged'}; - delete $args{'Privileged'}; + my $privileged = delete $args{'Privileged'}; if ($args{'CryptedPassword'} ) { @@ -226,8 +146,8 @@ sub Create { elsif ( !$args{'Password'} ) { $args{'Password'} = '*NO-PASSWORD*'; } - elsif ( length( $args{'Password'} ) < $RT::MinimumPasswordLength ) { - return ( 0, $self->loc("Password too short") ); + elsif ( length( $args{'Password'} ) < RT->Config->Get('MinimumPasswordLength') ) { + return ( 0, $self->loc("Password needs to be at least [_1] characters long",RT->Config->Get('MinimumPasswordLength')) ); } else { @@ -237,8 +157,6 @@ sub Create { #TODO Specify some sensible defaults. unless ( $args{'Name'} ) { - use Data::Dumper; - $RT::Logger->crit(Dumper \%args); return ( 0, $self->loc("Must specify 'Name' attribute") ); } @@ -248,8 +166,8 @@ sub Create { $TempUser->Load( $args{'Name'} ); return ( 0, $self->loc('Name in use') ) if ( $TempUser->Id ); - return ( 0, $self->loc('Email address in use') ) - unless ( $self->ValidateEmailAddress( $args{'EmailAddress'} ) ); + my ($val, $message) = $self->ValidateEmailAddress( $args{'EmailAddress'} ); + return (0, $message) unless ( $val ); } else { $RT::Logger->warning( "$self couldn't check for pre-existing users"); @@ -263,14 +181,15 @@ sub Create { my $principal_id = $principal->Create(PrincipalType => 'User', Disabled => $args{'Disabled'}, ObjectId => '0'); - $principal->__Set(Field => 'ObjectId', Value => $principal_id); # If we couldn't create a principal Id, get the fuck out. unless ($principal_id) { $RT::Handle->Rollback(); - $RT::Logger->crit("Couldn't create a Principal on new user create. Strange things are afoot at the circle K"); + $RT::Logger->crit("Couldn't create a Principal on new user create."); + $RT::Logger->crit("Strange things are afoot at the circle K"); return ( 0, $self->loc('Could not create user') ); } + $principal->__Set(Field => 'ObjectId', Value => $principal_id); delete $args{'Disabled'}; $self->SUPER::Create(id => $principal_id , %args); @@ -279,58 +198,71 @@ sub Create { #If the create failed. unless ($id) { $RT::Handle->Rollback(); - $RT::Logger->error("Could not create a new user - " .join('-'. %args)); + $RT::Logger->error("Could not create a new user - " .join('-', %args)); return ( 0, $self->loc('Could not create user') ); } - - #TODO post 2.0 - #if ($args{'SendWelcomeMessage'}) { - # #TODO: Check if the email exists and looks valid - # #TODO: Send the user a "welcome message" - #} - - - 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 groumembers"); + $RT::Logger->crit("Couldn't stash the user in groupmembers"); return ( 0, $self->loc('Could not create user') ); } - $RT::Handle->Commit; - #$RT::Logger->debug("Adding the user as a member of everyone"); my $everyone = RT::Group->new($self->CurrentUser); $everyone->LoadSystemInternalGroup('Everyone'); - $everyone->AddMember($self->PrincipalId); + 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') ); + } + + + my ($everyone_id, $everyone_msg) = $everyone->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId); + 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') ); + } + + my $access_class = RT::Group->new($self->CurrentUser); if ($privileged) { - my $priv = RT::Group->new($self->CurrentUser); - #$RT::Logger->debug("Making ".$self->Id." a privileged user"); - $priv->LoadSystemInternalGroup('Privileged'); - $priv->AddMember($self->PrincipalId); + $access_class->LoadSystemInternalGroup('Privileged'); } else { - my $unpriv = RT::Group->new($self->CurrentUser); - #$RT::Logger->debug("Making ".$self->Id." an unprivileged user"); - $unpriv->LoadSystemInternalGroup('Unprivileged'); - $unpriv->AddMember($self->PrincipalId); + $access_class->LoadSystemInternalGroup('Unprivileged'); } + 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') ); + } - # $RT::Logger->debug("Finished creating the user"); - return ( $id, $self->loc('User created') ); -} -# }}} + my ($ac_id, $ac_msg) = $access_class->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId); + + 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') ); + } + if ( $record_transaction ) { + $self->_NewTransaction( Type => "Create" ); + } -# {{{ SetPrivileged + $RT::Handle->Commit; + + return ( $id, $self->loc('User created') ); +} =head2 SetPrivileged BOOL @@ -339,20 +271,6 @@ Otherwise, makes this user a member of the "Unprivileged" pseudogroup. Returns a standard RT tuple of (val, msg); -=begin testing - - -ok(my $user = RT::User->new($RT::SystemUser)); -ok($user->Load('root'), "Loaded user 'root'"); -ok($user->Privileged, "User 'root' is privileged"); -ok(my ($v,$m) = $user->SetPrivileged(0)); -ok ($v ==1, "Set unprivileged suceeded ($m)"); -ok(!$user->Privileged, "User 'root' is no longer privileged"); -ok(my ($v2,$m2) = $user->SetPrivileged(1)); -ok ($v2 ==1, "Set privileged suceeded ($m2"); -ok($user->Privileged, "User 'root' is privileged again"); - -=end testing =cut @@ -360,9 +278,13 @@ sub SetPrivileged { my $self = shift; my $val = shift; + #Check the ACL + unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) { + return ( 0, $self->loc('Permission Denied') ); + } + my $priv = RT::Group->new($self->CurrentUser); $priv->LoadSystemInternalGroup('Privileged'); - unless ($priv->Id) { $RT::Logger->crit("Could not find Privileged pseudogroup"); return(0,$self->loc("Failed to find 'Privileged' users pseudogroup.")); @@ -375,13 +297,14 @@ sub SetPrivileged { return(0,$self->loc("Failed to find 'Unprivileged' users pseudogroup")); } + my $principal = $self->PrincipalId; if ($val) { - if ($priv->HasMember($self->PrincipalObj)) { + if ($priv->HasMember($principal)) { #$RT::Logger->debug("That user is already privileged"); return (0,$self->loc("That user is already privileged")); } - if ($unpriv->HasMember($self->PrincipalObj)) { - $unpriv->DeleteMember($self->PrincipalId); + if ($unpriv->HasMember($principal)) { + $unpriv->_DeleteMember($principal); } else { # if we had layered transactions, life would be good # sadly, we have to just go ahead, even if something @@ -389,7 +312,7 @@ sub SetPrivileged { $RT::Logger->crit("User ".$self->Id." is neither privileged nor ". "unprivileged. something is drastically wrong."); } - my ($status, $msg) = $priv->AddMember($self->PrincipalId); + my ($status, $msg) = $priv->_AddMember( InsideTransaction => 1, PrincipalId => $principal); if ($status) { return (1, $self->loc("That user is now privileged")); } else { @@ -397,12 +320,12 @@ sub SetPrivileged { } } else { - if ($unpriv->HasMember($self->PrincipalObj)) { + if ($unpriv->HasMember($principal)) { #$RT::Logger->debug("That user is already unprivileged"); return (0,$self->loc("That user is already unprivileged")); } - if ($priv->HasMember($self->PrincipalObj)) { - $priv->DeleteMember($self->PrincipalId); + if ($priv->HasMember($principal)) { + $priv->_DeleteMember( $principal ); } else { # if we had layered transactions, life would be good # sadly, we have to just go ahead, even if something @@ -410,7 +333,7 @@ sub SetPrivileged { $RT::Logger->crit("User ".$self->Id." is neither privileged nor ". "unprivileged. something is drastically wrong."); } - my ($status, $msg) = $unpriv->AddMember($self->PrincipalId); + my ($status, $msg) = $unpriv->_AddMember( InsideTransaction => 1, PrincipalId => $principal); if ($status) { return (1, $self->loc("That user is now unprivileged")); } else { @@ -419,10 +342,6 @@ sub SetPrivileged { } } -# }}} - -# {{{ Privileged - =head2 Privileged Returns true if this user is privileged. Returns undef otherwise. @@ -433,7 +352,7 @@ sub Privileged { my $self = shift; my $priv = RT::Group->new($self->CurrentUser); $priv->LoadSystemInternalGroup('Privileged'); - if ($priv->HasMember($self->PrincipalObj)) { + if ( $priv->HasMember( $self->PrincipalId ) ) { return(1); } else { @@ -441,10 +360,6 @@ sub Privileged { } } -# }}} - -# {{{ sub _BootstrapCreate - #create a user without validating _any_ data. #To be used only on database init. @@ -494,10 +409,6 @@ sub _BootstrapCreate { return ( $id, 'User created' ); } -# }}} - -# {{{ sub Delete - sub Delete { my $self = shift; @@ -505,40 +416,35 @@ sub Delete { } -# }}} - -# {{{ sub Load - =head2 Load Load a user object from the database. Takes a single argument. -If the argument is numerical, load by the column 'id'. Otherwise, load by -the "Name" column which is the user's textual username. +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. =cut sub Load { - my $self = shift; + my $self = shift; my $identifier = shift || return undef; - #if it's an int, load by id. otherwise, load by name. if ( $identifier !~ /\D/ ) { - $self->SUPER::LoadById($identifier); + return $self->SUPER::LoadById( $identifier ); + } + elsif ( UNIVERSAL::isa( $identifier, 'RT::User' ) ) { + return $self->SUPER::LoadById( $identifier->Id ); } else { - $self->LoadByCol( "Name", $identifier ); + return $self->LoadByCol( "Name", $identifier ); } } -# }}} - -# {{{ sub LoadByEmail - =head2 LoadByEmail Tries to load this user object from the database by the user's email address. - =cut sub LoadByEmail { @@ -552,18 +458,15 @@ sub LoadByEmail { $address = $self->CanonicalizeEmailAddress($address); - #$RT::Logger->debug("Trying to load an email address: $address\n"); + #$RT::Logger->debug("Trying to load an email address: $address"); return $self->LoadByCol( "EmailAddress", $address ); } -# }}} - -# {{{ LoadOrCreateByEmail - =head2 LoadOrCreateByEmail ADDRESS Attempts to find a user who has the provided email address. If that fails, creates an unprivileged user with -the provided email address. and loads them. +the provided email address and loads them. Address can be provided either as L object +or string which is parsed using the module. 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. @@ -574,47 +477,45 @@ sub LoadOrCreateByEmail { my $self = shift; my $email = shift; - my ($val, $message); - - $self->LoadByEmail($email); - $message = $self->loc('User loaded'); - unless ($self->Id) { - ( $val, $message ) = $self->Create( - Name => $email, - EmailAddress => $email, - RealName => $email, - 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"); - } - else { - $RT::Logger->crit("Failed to create user ".$email .": " .$message); - } - } - } + my ($message, $name); + if ( UNIVERSAL::isa( $email => 'Email::Address' ) ) { + ($email, $name) = ($email->address, $email->phrase); + } else { + ($email, $name) = RT::Interface::Email::ParseAddressFromHeader( $email ); + } - if ($self->Id) { - return($self->Id, $message); - } - else { - return(0, $message); + $self->LoadByEmail( $email ); + $self->Load( $email ) unless $self->Id; + $message = $self->loc('User loaded'); + + unless( $self->Id ) { + my $val; + ($val, $message) = $self->Create( + Name => $email, + EmailAddress => $email, + RealName => $name, + 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"); + } + else { + $RT::Logger->crit("Failed to create user ". $email .": " .$message); + } } - - } - -# }}} - -# {{{ sub ValidateEmailAddress + return (0, $message) unless $self->id; + return ($self->Id, $message); +} =head2 ValidateEmailAddress ADDRESS @@ -630,30 +531,94 @@ sub ValidateEmailAddress { # if the email address is null, it's always valid return (1) if ( !$Value || $Value eq "" ); + 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 ) ); + } + + my $TempUser = RT::User->new($RT::SystemUser); $TempUser->LoadByEmail($Value); - if ( $TempUser->id && ( $TempUser->id != $self->id ) ) + if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) ) { # if we found a user with that address # it's invalid to set this user's address to it - return (undef); + return ( 0, $self->loc('Email address in use') ); } else { #it's a valid email address return (1); } } -# }}} +=head2 SetEmailAddress -# {{{ sub CanonicalizeEmailAddress +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 { + my $self = shift; + my $Value = shift; + + my ($val, $message) = $self->ValidateEmailAddress( $Value ); + if ( $val ) { + return $self->_Set( Field => 'EmailAddress', Value => $Value ); + } else { + return ( 0, $message ) + } + +} + +=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 CanonicalizeEmailAddress ADDRESS +=item empty string returned otherwise. -# CanonicalizeEmailAddress converts email addresses into canonical form. -# it takes one email address in and returns the proper canonical -# form. You can dump whatever your proper local config is in here +=back + +=cut + +sub EmailFrequency { + my $self = shift; + my %args = ( + Ticket => undef, + @_ + ); + return '' unless $self->id && $self->id != $RT::Nobody->id + && $self->id != $RT::SystemUser->id; + return 'no email' unless my $email = $self->EmailAddress; + return 'squelched' if $args{'Ticket'} && + 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 ''; +} + +=head2 CanonicalizeEmailAddress ADDRESS + +CanonicalizeEmailAddress converts email addresses into canonical form. +it takes one email address in and returns the proper canonical +form. You can dump whatever your proper local config is in here. Note +that it may be called as a static method; in this case the first argument +is class name not an object. =cut @@ -663,21 +628,37 @@ sub CanonicalizeEmailAddress { # Example: the following rule would treat all email # coming from a subdomain as coming from second level domain # foo.com - if ($RT::CanonicalizeEmailAddressMatch && $RT::CanonicalizeEmailAddressReplace ) { - $email =~ s/$RT::CanonicalizeEmailAddressMatch/$RT::CanonicalizeEmailAddressReplace/gi; + if ( my $match = RT->Config->Get('CanonicalizeEmailAddressMatch') and + my $replace = RT->Config->Get('CanonicalizeEmailAddressReplace') ) + { + $email =~ s/$match/$replace/gi; } return ($email); } +=head2 CanonicalizeUserInfo HASH of ARGS -# }}} +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. + +This function is intended to allow users to have their info looked up via +an outside source and modified upon creation. + +=cut +sub CanonicalizeUserInfo { + my $self = shift; + my $args = shift; + my $success = 1; + + return ($success); +} -# {{{ Password related functions -# {{{ sub SetRandomPassword +=head2 Password and authentication related functions -=head2 SetRandomPassword +=head3 SetRandomPassword 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. @@ -692,7 +673,11 @@ sub SetRandomPassword { return ( 0, $self->loc("Permission Denied") ); } - my $pass = $self->GenerateRandomPassword( 6, 8 ); + + my $min = ( RT->Config->Get('MinimumPasswordLength') > 6 ? RT->Config->Get('MinimumPasswordLength') : 6); + my $max = ( RT->Config->Get('MinimumPasswordLength') > 8 ? RT->Config->Get('MinimumPasswordLength') : 8); + + my $pass = $self->GenerateRandomPassword( $min, $max) ; # If we have "notify user on @@ -706,11 +691,7 @@ sub SetRandomPassword { } -# }}} - -# {{{ sub ResetPassword - -=head2 ResetPassword +=head3 ResetPassword Returns status, [ERROR or new password]. Resets this user\'s password to a randomly generated pronouncable password and emails them, using a @@ -732,38 +713,13 @@ sub ResetPassword { return ( 0, "$pass" ); } - my $template = RT::Template->new( $self->CurrentUser ); - - if ( $self->Privileged ) { - $template->LoadGlobalTemplate('RT_PasswordChange_Privileged'); - } - else { - $template->LoadGlobalTemplate('RT_PasswordChange_Privileged'); - } - - unless ( $template->Id ) { - $template->LoadGlobalTemplate('RT_PasswordChange'); - } - - unless ( $template->Id ) { - $RT::Logger->crit( "$self tried to send " - . $self->Name - . " a password reminder " - . "but couldn't find a password change template" ); - } - - my $notification = RT::Action::SendPasswordEmail->new( - TemplateObj => $template, - Argument => $pass - ); - - $notification->SetTo( $self->EmailAddress ); - - my ($ret); - $ret = $notification->Prepare(); - if ($ret) { - $ret = $notification->Commit(); - } + my $ret = RT::Interface::Email::SendEmailUsingTemplate( + To => $self->EmailAddress, + Template => 'PasswordChange', + Arguments => { + NewPassword => $pass, + }, + ); if ($ret) { return ( 1, $self->loc('New password notification sent') ); @@ -774,11 +730,7 @@ sub ResetPassword { } -# }}} - -# {{{ sub GenerateRandomPassword - -=head2 GenerateRandomPassword MIN_LEN and MAX_LEN +=head3 GenerateRandomPassword MIN_LEN and MAX_LEN Returns a random password between MIN_LEN and MAX_LEN characters long. @@ -964,11 +916,7 @@ sub _GenerateRandomNextChar { return ($i); } -# }}} - -# {{{ sub SetPassword - -=head2 SetPassword +=head3 SetPassword Takes a string. Checks the string's length and sets this user's password to that string. @@ -980,25 +928,33 @@ sub SetPassword { my $password = shift; unless ( $self->CurrentUserCanModify('Password') ) { - return ( 0, $self->loc('Permission Denied') ); + return ( 0, $self->loc('Password: Permission Denied') ); } if ( !$password ) { return ( 0, $self->loc("No password set") ); } - elsif ( length($password) < $RT::MinimumPasswordLength ) { - return ( 0, $self->loc("Password too short") ); + elsif ( length($password) < RT->Config->Get('MinimumPasswordLength') ) { + return ( 0, $self->loc("Password needs to be at least [_1] characters long", RT->Config->Get('MinimumPasswordLength')) ); } else { + my $new = !$self->HasPassword; $password = $self->_GeneratePassword($password); - return ( $self->SUPER::SetPassword( $password)); + my ( $val, $msg ) = $self->SUPER::SetPassword($password); + if ($val) { + return ( 1, $self->loc("Password set") ) if $new; + return ( 1, $self->loc("Password changed") ); + } + else { + return ( $val, $msg ); + } } } -=head2 _GeneratePassword PASSWORD +=head3 _GeneratePassword PASSWORD -returns an MD5 hash of the password passed in, in base64 encoding. +returns an MD5 hash of the password passed in, in hexadecimal encoding. =cut @@ -1007,16 +963,44 @@ sub _GeneratePassword { my $password = shift; my $md5 = Digest::MD5->new(); - $md5->add($password); + $md5->add(encode_utf8($password)); + return ($md5->hexdigest); + +} + +=head3 _GeneratePasswordBase64 PASSWORD + +returns an MD5 hash of the password passed in, in base64 encoding +(obsoleted now). + +=cut + +sub _GeneratePasswordBase64 { + my $self = shift; + my $password = shift; + + my $md5 = Digest::MD5->new(); + $md5->add(encode_utf8($password)); return ($md5->b64digest); } -# }}} +=head3 HasPassword + +Returns true if the user has a valid password, otherwise returns false. + +=cut -# {{{ sub IsPassword +sub HasPassword { + my $self = shift; + my $pwd = $self->__Value('Password'); + return undef if !defined $pwd + || $pwd eq '' + || $pwd eq '*NO-PASSWORD*'; + return 1; +} -=head2 IsPassword +=head3 IsPassword Returns true if the passed in value is this user's password. Returns undef otherwise. @@ -1040,8 +1024,7 @@ sub IsPassword { return (undef); } - if ( ($self->__Value('Password') eq '') || - ($self->__Value('Password') eq undef) ) { + unless ($self->HasPassword) { return(undef); } @@ -1051,9 +1034,12 @@ sub IsPassword { } # if it's a historical password we say ok. - - if ( $self->__Value('Password') eq crypt( $value, $self->__Value('Password') ) ) { - return (1); + if ($self->__Value('Password') eq crypt($value, $self->__Value('Password')) + or $self->_GeneratePasswordBase64($value) eq $self->__Value('Password')) + { + # ...but upgrade the legacy password inplace. + $self->SUPER::SetPassword( $self->_GeneratePassword($value) ); + return(1); } # no password check has succeeded. get out @@ -1061,13 +1047,80 @@ sub IsPassword { return (undef); } -# }}} +=head3 AuthToken -# }}} +Returns an authentication string associated with the user. This +string can be used to generate passwordless URLs to integrate +RT with services and programms like callendar managers, rss +readers and other. + +=cut -# {{{ sub SetDisabled +sub AuthToken { + my $self = shift; + my $secret = $self->FirstAttribute("AuthToken"); + return $secret->Content if $secret; + + my $id = $self->id; + $self = RT::User->new( $RT::SystemUser ); + $self->Load( $id ); + $secret = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16); + my ($status, $msg) = $self->SetAttribute( Name => "AuthToken", Content => $secret ); + unless ( $status ) { + $RT::Logger->error( "Couldn't set auth token: $msg" ); + return undef; + } + return $secret; +} -=head2 Sub SetDisabled +=head3 GenerateAuthToken + +Generate a random authentication string for the user. + +=cut + +sub GenerateAuthToken { + my $self = shift; + my $token = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16); + return $self->SetAttribute( Name => "AuthToken", Content => $token ); +} + +=head3 GenerateAuthString + +Takes a string and returns back a hex hash string. Later you can use +this pair to make sure it's generated by this user using L + +=cut + +sub GenerateAuthString { + my $self = shift; + my $protect = shift; + + my $str = $self->AuthToken . $protect; + utf8::encode($str); + + return substr(Digest::MD5::md5_hex($str),0,16); +} + +=head3 ValidateAuthString + +Takes auth string and protected string. Returns true is protected string +has been protected by user's L. See also L. + +=cut + +sub ValidateAuthString { + my $self = shift; + my $auth_string = shift; + my $protected = shift; + + my $str = $self->AuthToken . $protected; + utf8::encode( $str ); + + return $auth_string eq substr(Digest::MD5::md5_hex($str),0,16); +} + +=head2 SetDisabled Toggles the user's disabled flag. If this flag is @@ -1076,53 +1129,69 @@ user will fail. The user will appear in no user listings. =cut -# }}} - sub SetDisabled { my $self = shift; + my $val = shift; unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) { return (0, $self->loc('Permission Denied')); } - return $self->PrincipalObj->SetDisabled(@_); + + $RT::Handle->BeginTransaction(); + my $set_err = $self->PrincipalObj->SetDisabled($val); + unless ($set_err) { + $RT::Handle->Rollback(); + $RT::Logger->warning(sprintf("Couldn't %s user %s", ($val == 1) ? "disable" : "enable", $self->PrincipalObj->Id)); + return (undef); + } + $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" ); + + $RT::Handle->Commit(); + + if ( $val == 1 ) { + return (1, $self->loc("User disabled")); + } else { + return (1, $self->loc("User enabled")); + } + } +=head2 Disabled + +Returns true if user is disabled or false otherwise + +=cut + sub Disabled { my $self = shift; return $self->PrincipalObj->Disabled(@_); } - -# {{{ Principal related routines - =head2 PrincipalObj Returns the principal object for this user. returns an empty RT::Principal if there's no principal object matching this user. The response is cached. PrincipalObj should never ever change. -=begin testing - -ok(my $u = RT::User->new($RT::SystemUser)); -ok($u->Load(1), "Loaded the first user"); -ok($u->PrincipalObj->ObjectId == 1, "user 1 is the first principal"); -ok($u->PrincipalObj->PrincipalType eq 'User' , "Principal 1 is a user, not a group"); - -=end testing - =cut - sub PrincipalObj { my $self = shift; - unless ($self->{'PrincipalObj'} && - ($self->{'PrincipalObj'}->ObjectId == $self->Id) && - ($self->{'PrincipalObj'}->PrincipalType eq 'User')) { - $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser); - $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id, - 'PrincipalType' => 'User') ; - } - return($self->{'PrincipalObj'}); + unless ( $self->id ) { + $RT::Logger->error("Couldn't get principal for not loaded object"); + return undef; + } + + my $obj = RT::Principal->new( $self->CurrentUser ); + $obj->LoadById( $self->id ); + unless ( $obj->id ) { + $RT::Logger->crit( 'No principal for user #'. $self->id ); + return undef; + } elsif ( $obj->PrincipalType ne 'User' ) { + $RT::Logger->crit( 'User #'. $self->id .' has principal of '. $obj->PrincipalType .' type' ); + return undef; + } + return $obj; } @@ -1137,12 +1206,6 @@ sub PrincipalId { return $self->Id; } -# }}} - - - -# {{{ sub HasGroupRight - =head2 HasGroupRight Takes a paramhash which can contain @@ -1173,14 +1236,11 @@ sub HasGroupRight { $args{'GroupObj'}->Load( $args{'Group'} ); } - # {{{ Validate and load up the GroupId + # Validate and load up the GroupId unless ( ( defined $args{'GroupObj'} ) and ( $args{'GroupObj'}->Id ) ) { return undef; } - # }}} - - # Figure out whether a user has the right we're asking about. my $retval = $self->HasRight( Object => $args{'GroupObj'}, @@ -1188,272 +1248,297 @@ sub HasGroupRight { ); return ($retval); - - } -# }}} - -# {{{ sub Rights testing - -=head2 Rights testing - - -=begin testing - -my $root = RT::User->new($RT::SystemUser); -$root->Load('root'); -ok($root->Id, "Found the root user"); -my $rootq = RT::Queue->new($root); -$rootq->Load(1); -ok($rootq->Id, "Loaded the first queue"); - -ok ($rootq->CurrentUser->HasRight(Right=> 'CreateTicket', Object => $rootq), "Root can create tickets"); - -my $new_user = RT::User->new($RT::SystemUser); -my ($id, $msg) = $new_user->Create(Name => 'ACLTest'); - -ok ($id, "Created a new user for acl test $msg"); - -my $q = RT::Queue->new($new_user); -$q->Load(1); -ok($q->Id, "Loaded the first queue"); - - -ok (!$q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "Some random user doesn't have the right to create tickets"); -ok (my ($gval, $gmsg) = $new_user->PrincipalObj->GrantRight( Right => 'CreateTicket', Object => $q), "Granted the random user the right to create tickets"); -ok ($gval, "Grant succeeded - $gmsg"); - - -ok ($q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "The user can create tickets after we grant him the right"); -ok (my ($gval, $gmsg) = $new_user->PrincipalObj->RevokeRight( Right => 'CreateTicket', Object => $q), "revoked the random user the right to create tickets"); -ok ($gval, "Revocation succeeded - $gmsg"); -ok (!$q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "The user can't create tickets anymore"); - - - - - -# Create a ticket in the queue -my $new_tick = RT::Ticket->new($RT::SystemUser); -my ($tickid, $tickmsg) = $new_tick->Create(Subject=> 'ACL Test', Queue => 'General'); -ok($tickid, "Created ticket: $tickid"); -# Make sure the user doesn't have the right to modify tickets in the queue -ok (!$new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); -# Create a new group -my $group = RT::Group->new($RT::SystemUser); -$group->CreateUserDefinedGroup(Name => 'ACLTest'); -ok($group->Id, "Created a new group Ok"); -# Grant a group the right to modify tickets in a queue -ok(my ($gv,$gm) = $group->PrincipalObj->GrantRight( Object => $q, Right => 'ModifyTicket'),"Granted the group the right to modify tickets"); -ok($gv,"Grant succeeed - $gm"); -# Add the user to the group -ok( my ($aid, $amsg) = $group->AddMember($new_user->PrincipalId), "Added the member to the group"); -ok ($aid, "Member added to group: $amsg"); -# Make sure the user does have the right to modify tickets in the queue -ok ($new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can modify the ticket with group membership"); - - -# Remove the user from the group -ok( my ($did, $dmsg) = $group->DeleteMember($new_user->PrincipalId), "Deleted the member from the group"); -ok ($did,"Deleted the group member: $dmsg"); -# Make sure the user doesn't have the right to modify tickets in the queue -ok (!$new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); - - -my $q_as_system = RT::Queue->new($RT::SystemUser); -$q_as_system->Load(1); -ok($q_as_system->Id, "Loaded the first queue"); - -# Create a ticket in the queue -my $new_tick2 = RT::Ticket->new($RT::SystemUser); -my ($tick2id, $tickmsg) = $new_tick2->Create(Subject=> 'ACL Test 2', Queue =>$q_as_system->Id); -ok($tick2id, "Created ticket: $tick2id"); -ok($new_tick2->QueueObj->id eq $q_as_system->Id, "Created a new ticket in queue 1"); +=head2 OwnGroups +Returns a group collection object containing the groups of which this +user is a member. -# make sure that the user can't do this without subgroup membership -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); - -# Create a subgroup -my $subgroup = RT::Group->new($RT::SystemUser); -$subgroup->CreateUserDefinedGroup(Name => 'Subgrouptest'); -ok($subgroup->Id, "Created a new group ".$subgroup->Id."Ok"); -#Add the subgroup as a subgroup of the group -my ($said, $samsg) = $group->AddMember($subgroup->PrincipalId); -ok ($said, "Added the subgroup as a member of the group"); -# Add the user to a subgroup of the group - -my ($usaid, $usamsg) = $subgroup->AddMember($new_user->PrincipalId); -ok($usaid,"Added the user ".$new_user->Id."to the subgroup"); -# Make sure the user does have the right to modify tickets in the queue -ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket with subgroup membership"); - -# {{{ Deal with making sure that members of subgroups of a disabled group don't have rights - -my ($id, $msg); - ($id, $msg) = $group->SetDisabled(1); - ok ($id,$msg); -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket when the group ".$group->Id. " is disabled"); - ($id, $msg) = $group->SetDisabled(0); -ok($id,$msg); -# Test what happens when we disable the group the user is a member of directly - -($id, $msg) = $subgroup->SetDisabled(1); - ok ($id,$msg); -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket when the group ".$subgroup->Id. " is disabled"); - ($id, $msg) = $subgroup->SetDisabled(0); - ok ($id,$msg); -ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket without group membership"); - -# }}} - - -my ($usrid, $usrmsg) = $subgroup->DeleteMember($new_user->PrincipalId); -ok($usrid,"removed the user from the group - $usrmsg"); -# Make sure the user doesn't have the right to modify tickets in the queue -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); - -#revoke the right to modify tickets in a queue -ok(($gv,$gm) = $group->PrincipalObj->RevokeRight( Object => $q, Right => 'ModifyTicket'),"Granted the group the right to modify tickets"); -ok($gv,"revoke succeeed - $gm"); - -# {{{ Test the user's right to modify a ticket as a _queue_ admincc for a right granted at the _queue_ level - -# Grant queue admin cc the right to modify ticket in the queue -ok(my ($qv,$qm) = $q_as_system->AdminCc->PrincipalObj->GrantRight( Object => $q_as_system, Right => 'ModifyTicket'),"Granted the queue adminccs the right to modify tickets"); -ok($qv, "Granted the right successfully - $qm"); - -# Add the user as a queue admincc -ok ((my $add_id, $add_msg) = $q_as_system->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc"); -ok ($add_id, "the user is now a queue admincc - $add_msg"); - -# Make sure the user does have the right to modify tickets in the queue -ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc"); -# Remove the user from the role group -ok ((my $del_id, $del_msg) = $q_as_system->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc"); - -# Make sure the user doesn't have the right to modify tickets in the queue -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); - -# }}} - -# {{{ Test the user's right to modify a ticket as a _ticket_ admincc with the right granted at the _queue_ level - -# Add the user as a ticket admincc -ok ((my $uadd_id, $uadd_msg) = $new_tick2->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc"); -ok ($add_id, "the user is now a queue admincc - $add_msg"); - -# Make sure the user does have the right to modify tickets in the queue -ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc"); - -# Remove the user from the role group -ok ((my $del_id, $del_msg) = $new_tick2->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc"); - -# Make sure the user doesn't have the right to modify tickets in the queue -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); - +=cut -# Revoke the right to modify ticket in the queue -ok(my ($rqv,$rqm) = $q_as_system->AdminCc->PrincipalObj->RevokeRight( Object => $q_as_system, Right => 'ModifyTicket'),"Revokeed the queue adminccs the right to modify tickets"); -ok($rqv, "Revoked the right successfully - $rqm"); +sub OwnGroups { + my $self = shift; + my $groups = RT::Groups->new($self->CurrentUser); + $groups->LimitToUserDefinedGroups; + $groups->WithMember(PrincipalId => $self->Id, + Recursively => 1); + return $groups; +} # }}} +# {{{ Links + +#much false laziness w/Ticket_Overlay.pm. now with RT 3.8! + +# A helper table for links mapping to make it easier +# to build and parse links between tickets + +use vars '%LINKDIRMAP'; + +%LINKDIRMAP = ( + MemberOf => { Base => 'MemberOf', + Target => 'HasMember', }, + RefersTo => { Base => 'RefersTo', + Target => 'ReferredToBy', }, + DependsOn => { Base => 'DependsOn', + Target => 'DependedOnBy', }, + MergedInto => { Base => 'MergedInto', + Target => 'MergedInto', }, + +); + +sub LINKDIRMAP { return \%LINKDIRMAP } + +#sub _Links { +# my $self = shift; +# +# #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic --- +# #tobias meant by $f +# my $field = shift; +# my $type = shift || ""; +# +# unless ( $self->{"$field$type"} ) { +# $self->{"$field$type"} = new RT::Links( $self->CurrentUser ); +# if ( $self->CurrentUserHasRight('ShowTicket') ) { +# # Maybe this ticket is a merged ticket +# my $Tickets = new RT::Tickets( $self->CurrentUser ); +# # at least to myself +# $self->{"$field$type"}->Limit( FIELD => $field, +# VALUE => $self->URI, +# ENTRYAGGREGATOR => 'OR' ); +# $Tickets->Limit( FIELD => 'EffectiveId', +# VALUE => $self->EffectiveId ); +# while (my $Ticket = $Tickets->Next) { +# $self->{"$field$type"}->Limit( FIELD => $field, +# VALUE => $Ticket->URI, +# ENTRYAGGREGATOR => 'OR' ); +# } +# $self->{"$field$type"}->Limit( FIELD => 'Type', +# VALUE => $type ) +# if ($type); +# } +# } +# return ( $self->{"$field$type"} ); +#} + +=head2 DeleteLink + +Delete a link. takes a paramhash of Base, Target and Type. +Either Base or Target must be null. The null value will +be replaced with this ticket\'s id +=cut -# {{{ Test the user's right to modify a ticket as a _queue_ admincc for a right granted at the _system_ level - -# Before we start Make sure the user does not have the right to modify tickets in the queue -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without it being granted"); -ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue without it being granted"); - -# Grant queue admin cc the right to modify ticket in the queue -ok(my ($qv,$qm) = $q_as_system->AdminCc->PrincipalObj->GrantRight( Object => $RT::System, Right => 'ModifyTicket'),"Granted the queue adminccs the right to modify tickets"); -ok($qv, "Granted the right successfully - $qm"); - -# Make sure the user can't modify the ticket before they're added as a watcher -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without being an admincc"); -ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue without being an admincc"); - -# Add the user as a queue admincc -ok ((my $add_id, $add_msg) = $q_as_system->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc"); -ok ($add_id, "the user is now a queue admincc - $add_msg"); - -# Make sure the user does have the right to modify tickets in the queue -ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc"); -ok ($new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can modify tickets in the queue as an admincc"); -# Remove the user from the role group -ok ((my $del_id, $del_msg) = $q_as_system->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc"); - -# Make sure the user doesn't have the right to modify tickets in the queue -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); -ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can't modify tickets in the queue without group membership"); +sub DeleteLink { + my $self = shift; + my %args = ( + Base => undef, + Target => undef, + Type => undef, + @_ + ); -# }}} + unless ( $args{'Target'} || $args{'Base'} ) { + $RT::Logger->error("Base or Target must be specified\n"); + return ( 0, $self->loc('Either base or target must be specified') ); + } -# {{{ Test the user's right to modify a ticket as a _ticket_ admincc with the right granted at the _queue_ level + #check acls + my $right = 0; + $right++ if $self->CurrentUserHasRight('AdminUsers'); + if ( !$right && $RT::StrictLinkACL ) { + return ( 0, $self->loc("Permission Denied") ); + } -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without being an admincc"); -ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj without being an admincc"); +# # If the other URI is an RT::Ticket, we want to make sure the user +# # can modify it too... +# my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} ); +# return (0, $msg) unless $status; +# if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) { +# $right++; +# } +# if ( ( !$RT::StrictLinkACL && $right == 0 ) || +# ( $RT::StrictLinkACL && $right < 2 ) ) +# { +# return ( 0, $self->loc("Permission Denied") ); +# } + + my ($val, $Msg) = $self->SUPER::_DeleteLink(%args); + + if ( !$val ) { + $RT::Logger->debug("Couldn't find that link\n"); + return ( 0, $Msg ); + } + my ($direction, $remote_link); -# Add the user as a ticket admincc -ok ((my $uadd_id, $uadd_msg) = $new_tick2->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc"); -ok ($add_id, "the user is now a queue admincc - $add_msg"); + if ( $args{'Base'} ) { + $remote_link = $args{'Base'}; + $direction = 'Target'; + } + elsif ( $args{'Target'} ) { + $remote_link = $args{'Target'}; + $direction='Base'; + } -# Make sure the user does have the right to modify tickets in the queue -ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc"); -ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj being only a ticket admincc"); + if ( $args{'Silent'} ) { + return ( $val, $Msg ); + } + else { + my $remote_uri = RT::URI->new( $self->CurrentUser ); + $remote_uri->FromURI( $remote_link ); + + my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( + Type => 'DeleteLink', + Field => $LINKDIRMAP{$args{'Type'}}->{$direction}, + OldValue => $remote_uri->URI || $remote_link, + TimeTaken => 0 + ); + + if ( $remote_uri->IsLocal ) { + + my $OtherObj = $remote_uri->Object; + my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink', + Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} + : $LINKDIRMAP{$args{'Type'}}->{Target}, + OldValue => $self->URI, + ActivateScrips => ! $RT::LinkTransactionsRun1Scrip, + TimeTaken => 0 ); + } -# Remove the user from the role group -ok ((my $del_id, $del_msg) = $new_tick2->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc"); + return ( $Trans, $Msg ); + } +} -# Make sure the user doesn't have the right to modify tickets in the queue -ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without being an admincc"); -ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj without being an admincc"); +sub AddLink { + my $self = shift; + my %args = ( Target => '', + Base => '', + Type => '', + Silent => undef, + @_ ); + + unless ( $args{'Target'} || $args{'Base'} ) { + $RT::Logger->error("Base or Target must be specified\n"); + return ( 0, $self->loc('Either base or target must be specified') ); + } + my $right = 0; + $right++ if $self->CurrentUserHasRight('AdminUsers'); + if ( !$right && $RT::StrictLinkACL ) { + return ( 0, $self->loc("Permission Denied") ); + } -# Revoke the right to modify ticket in the queue -ok(my ($rqv,$rqm) = $q_as_system->AdminCc->PrincipalObj->RevokeRight( Object => $RT::System, Right => 'ModifyTicket'),"Revokeed the queue adminccs the right to modify tickets"); -ok($rqv, "Revoked the right successfully - $rqm"); +# # If the other URI is an RT::Ticket, we want to make sure the user +# # can modify it too... +# my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} ); +# return (0, $msg) unless $status; +# if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) { +# $right++; +# } +# if ( ( !$RT::StrictLinkACL && $right == 0 ) || +# ( $RT::StrictLinkACL && $right < 2 ) ) +# { +# return ( 0, $self->loc("Permission Denied") ); +# } + + return $self->_AddLink(%args); +} -# }}} +#sub __GetTicketFromURI { +# my $self = shift; +# my %args = ( URI => '', @_ ); +# +# # If the other URI is an RT::Ticket, we want to make sure the user +# # can modify it too... +# my $uri_obj = RT::URI->new( $self->CurrentUser ); +# $uri_obj->FromURI( $args{'URI'} ); +# +# unless ( $uri_obj->Resolver && $uri_obj->Scheme ) { +# my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} ); +# $RT::Logger->warning( "$msg\n" ); +# return( 0, $msg ); +# } +# my $obj = $uri_obj->Resolver->Object; +# unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) { +# return (1, 'Found not a ticket', undef); +# } +# return (1, 'Found ticket', $obj); +#} + +=head2 _AddLink + +Private non-acled variant of AddLink so that links can be added during create. +=cut +sub _AddLink { + my $self = shift; + my %args = ( Target => '', + Base => '', + Type => '', + Silent => undef, + @_ ); + + my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args); + return ($val, $msg) if !$val || $exist; + + my ($direction, $remote_link); + if ( $args{'Target'} ) { + $remote_link = $args{'Target'}; + $direction = 'Base'; + } elsif ( $args{'Base'} ) { + $remote_link = $args{'Base'}; + $direction = 'Target'; + } + # Don't write the transaction if we're doing this on create + if ( $args{'Silent'} ) { + return ( $val, $msg ); + } + else { + my $remote_uri = RT::URI->new( $self->CurrentUser ); + $remote_uri->FromURI( $remote_link ); + + #Write the transaction + my ( $Trans, $Msg, $TransObj ) = + $self->_NewTransaction(Type => 'AddLink', + Field => $LINKDIRMAP{$args{'Type'}}->{$direction}, + NewValue => $remote_uri->URI || $remote_link, + TimeTaken => 0 ); + + if ( $remote_uri->IsLocal ) { + + my $OtherObj = $remote_uri->Object; + my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink', + Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} + : $LINKDIRMAP{$args{'Type'}}->{Target}, + NewValue => $self->URI, + ActivateScrips => ! $RT::LinkTransactionsRun1Scrip, + TimeTaken => 0 ); + } + return ( $val, $Msg ); + } -# Grant "privileged users" the system right to create users -# Create a privileged user. -# have that user create another user -# Revoke the right for privileged users to create users -# have the privileged user try to create another user and fail the ACL check +} -=end testing -=cut # }}} +=head2 HasRight -# {{{ sub HasRight - -=head2 sub HasRight - -Shim around PrincipalObj->HasRight. See RT::Principal +Shim around PrincipalObj->HasRight. See L. =cut sub HasRight { - my $self = shift; return $self->PrincipalObj->HasRight(@_); } -# }}} - -# {{{ sub CurrentUserCanModify - =head2 CurrentUserCanModify RIGHT If the user has rights for this object, either because @@ -1491,14 +1576,10 @@ sub CurrentUserCanModify { } -# }}} - -# {{{ sub CurrentUserHasRight - =head2 CurrentUserHasRight - Takes a single argument. returns 1 if $Self->CurrentUser - has the requested right. returns undef otherwise +Takes a single argument. returns 1 if $Self->CurrentUser +has the requested right. returns undef otherwise =cut @@ -1509,9 +1590,206 @@ sub CurrentUserHasRight { return ( $self->CurrentUser->HasRight(Right => $right, Object => $RT::System) ); } -# }}} +sub _PrefName { + my $name = shift; + if (ref $name) { + $name = ref($name).'-'.$name->Id; + } + + return 'Pref-'.$name; +} + +=head2 Preferences NAME/OBJ DEFAULT + +Obtain user preferences associated with given object or name. +Returns DEFAULT if no preferences found. If DEFAULT is a hashref, +override the entries with user preferences. + +=cut + +sub Preferences { + my $self = shift; + my $name = _PrefName (shift); + my $default = shift; + + my $attr = RT::Attribute->new( $self->CurrentUser ); + $attr->LoadByNameAndObject( Object => $self, Name => $name ); + + my $content = $attr->Id ? $attr->Content : undef; + unless ( ref $content eq 'HASH' ) { + return defined $content ? $content : $default; + } + + if (ref $default eq 'HASH') { + for (keys %$default) { + exists $content->{$_} or $content->{$_} = $default->{$_}; + } + } + elsif (defined $default) { + $RT::Logger->error("Preferences $name for user".$self->Id." is hash but default is not"); + } + return $content; +} + +=head2 SetPreferences NAME/OBJ VALUE + +Set user preferences associated with given object or name. + +=cut + +sub SetPreferences { + my $self = shift; + my $name = _PrefName( shift ); + my $value = shift; + + return (0, $self->loc("No permission to set preferences")) + unless $self->CurrentUserCanModify('Preferences'); + + my $attr = RT::Attribute->new( $self->CurrentUser ); + $attr->LoadByNameAndObject( Object => $self, Name => $name ); + if ( $attr->Id ) { + return $attr->SetContent( $value ); + } + else { + return $self->AddAttribute( Name => $name, Content => $value ); + } +} + +=head2 WatchedQueues ROLE_LIST + +Returns a RT::Queues object containing every queue watched by the user. + +Takes a list of roles which is some subset of ('Cc', 'AdminCc'). Defaults to: + +$user->WatchedQueues('Cc', 'AdminCc'); + +=cut + +sub WatchedQueues { + + my $self = shift; + my @roles = @_ || ('Cc', 'AdminCc'); + + $RT::Logger->debug('WatcheQueues got user ' . $self->Name); + + my $watched_queues = RT::Queues->new($self->CurrentUser); + + my $group_alias = $watched_queues->Join( + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'Groups', + FIELD2 => 'Instance', + ); + + $watched_queues->Limit( + ALIAS => $group_alias, + FIELD => 'Domain', + VALUE => 'RT::Queue-Role', + ENTRYAGGREGATOR => 'AND', + ); + if (grep { $_ eq 'Cc' } @roles) { + $watched_queues->Limit( + SUBCLAUSE => 'LimitToWatchers', + ALIAS => $group_alias, + FIELD => 'Type', + VALUE => 'Cc', + ENTRYAGGREGATOR => 'OR', + ); + } + if (grep { $_ eq 'AdminCc' } @roles) { + $watched_queues->Limit( + SUBCLAUSE => 'LimitToWatchers', + ALIAS => $group_alias, + FIELD => 'Type', + VALUE => 'AdminCc', + ENTRYAGGREGATOR => 'OR', + ); + } + + my $queues_alias = $watched_queues->Join( + ALIAS1 => $group_alias, + FIELD1 => 'id', + TABLE2 => 'CachedGroupMembers', + FIELD2 => 'GroupId', + ); + $watched_queues->Limit( + ALIAS => $queues_alias, + FIELD => 'MemberId', + VALUE => $self->PrincipalId, + ); + + $RT::Logger->debug("WatchedQueues got " . $watched_queues->Count . " queues"); + + return $watched_queues; + +} + +=head2 _CleanupInvalidDelegations { InsideTransaction => undef } + +Revokes all ACE entries delegated by this user which are inconsistent +with their current delegation rights. Does not perform permission +checks. Should only ever be called from inside the RT library. -# {{{ sub _Set +If called from inside a transaction, specify a true value for the +InsideTransaction parameter. + +Returns a true value if the deletion succeeded; returns a false value +and logs an internal error if the deletion fails (should not happen). + +=cut + +# XXX Currently there is a _CleanupInvalidDelegations method in both +# RT::User and RT::Group. If the recursive cleanup call for groups is +# ever unrolled and merged, this code will probably want to be +# factored out into RT::Principal. + +sub _CleanupInvalidDelegations { + my $self = shift; + my %args = ( InsideTransaction => undef, + @_ ); + + unless ( $self->Id ) { + $RT::Logger->warning("User not loaded."); + return (undef); + } + + my $in_trans = $args{InsideTransaction}; + + return(1) if ($self->HasRight(Right => 'DelegateRights', + Object => $RT::System)); + + # Look up all delegation rights currently posessed by this user. + my $deleg_acl = RT::ACL->new($RT::SystemUser); + $deleg_acl->LimitToPrincipal(Type => 'User', + Id => $self->PrincipalId, + IncludeGroupMembership => 1); + $deleg_acl->Limit( FIELD => 'RightName', + OPERATOR => '=', + VALUE => 'DelegateRights' ); + my @allowed_deleg_objects = map {$_->Object()} + @{$deleg_acl->ItemsArrayRef()}; + + # Look up all rights delegated by this principal which are + # inconsistent with the allowed delegation objects. + my $acl_to_del = RT::ACL->new($RT::SystemUser); + $acl_to_del->DelegatedBy(Id => $self->Id); + foreach (@allowed_deleg_objects) { + $acl_to_del->LimitNotObject($_); + } + + # Delete all disallowed delegations + while ( my $ace = $acl_to_del->Next() ) { + my $ret = $ace->_Delete(InsideTransaction => 1); + unless ($ret) { + $RT::Handle->Rollback() unless $in_trans; + $RT::Logger->warning("Couldn't delete delegated ACL entry ".$ace->Id); + return (undef); + } + } + + $RT::Handle->Commit() unless $in_trans; + return (1); +} sub _Set { my $self = shift; @@ -1519,6 +1797,8 @@ sub _Set { my %args = ( Field => undef, Value => undef, + TransactionType => 'Set', + RecordTransaction => 1, @_ ); @@ -1532,19 +1812,31 @@ sub _Set { return ( 0, $self->loc("Permission Denied") ); } - #Set the new value - my ( $ret, $msg ) = $self->SUPER::_Set( - Field => $args{'Field'}, - Value => $args{'Value'} - ); - - return ( $ret, $msg ); + my $Old = $self->SUPER::_Value("$args{'Field'}"); + + my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'}, + Value => $args{'Value'} ); + + #If we can't actually set the field to the value, don't record + # a transaction. instead, get out of here. + if ( $ret == 0 ) { return ( 0, $msg ); } + + if ( $args{'RecordTransaction'} == 1 ) { + + my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( + Type => $args{'TransactionType'}, + Field => $args{'Field'}, + NewValue => $args{'Value'}, + OldValue => $Old, + TimeTaken => $args{'TimeTaken'}, + ); + return ( $Trans, scalar $TransObj->BriefDescription ); + } + else { + return ( $ret, $msg ); + } } -# }}} - -# {{{ sub _Value - =head2 _Value Takes the name of a table column. @@ -1572,7 +1864,7 @@ sub _Value { #If the user wants to see their own values, let them # TODO figure ouyt a better way to deal with this - elsif ( $self->CurrentUser->Id == $self->Id ) { + elsif ( defined($self->Id) && $self->CurrentUser->Id == $self->Id ) { return ( $self->SUPER::_Value($field) ); } @@ -1586,8 +1878,98 @@ sub _Value { } -# }}} +=head2 FriendlyName + +Return the friendly name +=cut + +sub FriendlyName { + my $self = shift; + return $self->RealName if defined($self->RealName); + return $self->Name if defined($self->Name); + return ""; +} + +=head2 PreferredKey + +Returns the preferred key of the user. If none is set, then this will query +GPG and set the preferred key to the maximally trusted key found (and then +return it). Returns C if no preferred key can be found. + +=cut + +sub PreferredKey +{ + my $self = shift; + return undef unless RT->Config->Get('GnuPG')->{'Enable'}; + my $prefkey = $self->FirstAttribute('PreferredKey'); + return $prefkey->Content if $prefkey; + + # we don't have a preferred key for this user, so now we must query GPG + require RT::Crypt::GnuPG; + my %res = RT::Crypt::GnuPG::GetKeysForEncryption($self->EmailAddress); + return undef unless defined $res{'info'}; + my @keys = @{ $res{'info'} }; + return undef if @keys == 0; + + if (@keys == 1) { + $prefkey = $keys[0]->{'Fingerprint'}; + } + else { + # prefer the maximally trusted key + @keys = sort { $b->{'TrustLevel'} <=> $a->{'TrustLevel'} } @keys; + $prefkey = $keys[0]->{'Fingerprint'}; + } + + $self->SetAttribute(Name => 'PreferredKey', Content => $prefkey); + return $prefkey; +} + +sub PrivateKey { + my $self = shift; + + my $key = $self->FirstAttribute('PrivateKey') or return undef; + return $key->Content; +} + +sub SetPrivateKey { + my $self = shift; + my $key = shift; + # XXX: ACL + unless ( $key ) { + my ($status, $msg) = $self->DeleteAttribute('PrivateKey'); + unless ( $status ) { + $RT::Logger->error( "Couldn't delete attribute: $msg" ); + return ($status, $self->loc("Couldn't unset private key")); + } + return ($status, $self->loc("Unset private key")); + } + + # check that it's really private key + { + my %tmp = RT::Crypt::GnuPG::GetKeysForSigning( $key ); + return (0, $self->loc("No such key or it's not suitable for signing")) + if $tmp{'exit_code'} || !$tmp{'info'}; + } + + my ($status, $msg) = $self->SetAttribute( + Name => 'PrivateKey', + Content => $key, + ); + return ($status, $self->loc("Couldn't set private key")) + unless $status; + return ($status, $self->loc("Unset private key")); +} + +sub BasicColumns { + ( + [ Name => 'User Id' ], + [ EmailAddress => 'Email' ], + [ RealName => 'Name' ], + [ Organization => 'Organization' ], + ); +} 1;