X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FUser_Overlay.pm;h=fd83630d5173360e4cd827e5f9fb33a9efbc4308;hb=2dfda73eeb3eae2d4f894099754794ef07d060dd;hp=e828ebd719df626377fd0360bd51d44e831704fe;hpb=945721f48f74d5cfffef7c7cf3a3d6bc2521f5dd;p=freeside.git diff --git a/rt/lib/RT/User_Overlay.pm b/rt/lib/RT/User_Overlay.pm index e828ebd71..fd83630d5 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: +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# # -# (Except where explictly superceded by other copyright notices) +# (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,31 @@ # 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 @@ -43,6 +67,9 @@ ok(require RT::User); =cut + +package RT::User; + use strict; no warnings qw(redefine); @@ -53,85 +80,34 @@ use vars qw(%_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 }, + + } +} + # }}} @@ -147,41 +123,41 @@ sub _ClassAccessible { 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'); +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'); +($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'); +($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'); +($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'); +($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 => ''); +($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 => ''); +($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'); +($id,$msg) = $u7->SetEmailAddress('foo@bar'.$$); ok ($id, $msg); # can we change the address back to ""; ($id,$msg) = $u7->SetEmailAddress(''); @@ -200,11 +176,12 @@ sub Create { Privileged => 0, Disabled => 0, EmailAddress => '', + _RecordTransaction => 1, @_ # get the real argumentlist ); - - $args{'EmailAddress'} = $self->CanonicalizeEmailAddress($args{'EmailAddress'}); + # 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) ) { @@ -212,6 +189,17 @@ sub Create { } + 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'}; @@ -225,7 +213,7 @@ sub Create { $args{'Password'} = '*NO-PASSWORD*'; } elsif ( length( $args{'Password'} ) < $RT::MinimumPasswordLength ) { - return ( 0, $self->loc("Password too short") ); + return ( 0, $self->loc("Password needs to be at least [_1] characters long",$RT::MinimumPasswordLength) ); } else { @@ -234,7 +222,9 @@ sub Create { #TODO Specify some sensible defaults. - unless ( defined( $args{'Name'} ) ) { + unless ( $args{'Name'} ) { + use Data::Dumper; + $RT::Logger->crit(Dumper \%args); return ( 0, $self->loc("Must specify 'Name' attribute") ); } @@ -259,14 +249,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); @@ -274,50 +265,70 @@ sub Create { #If the create failed. unless ($id) { - $RT::Logger->error("Could not create a new user - " .join('-'. %args)); + $RT::Handle->Rollback(); + $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') ); + } + + + 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" ); } + $RT::Handle->Commit; - # $RT::Logger->debug("Finished creating the user"); return ( $id, $self->loc('User created') ); } @@ -355,6 +366,10 @@ 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'); @@ -376,7 +391,7 @@ sub SetPrivileged { return (0,$self->loc("That user is already privileged")); } if ($unpriv->HasMember($self->PrincipalObj)) { - $unpriv->DeleteMember($self->PrincipalId); + $unpriv->_DeleteMember($self->PrincipalId); } else { # if we had layered transactions, life would be good # sadly, we have to just go ahead, even if something @@ -384,7 +399,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 => $self->PrincipalId); if ($status) { return (1, $self->loc("That user is now privileged")); } else { @@ -397,7 +412,7 @@ sub SetPrivileged { return (0,$self->loc("That user is already unprivileged")); } if ($priv->HasMember($self->PrincipalObj)) { - $priv->DeleteMember($self->PrincipalId); + $priv->_DeleteMember( $self->PrincipalId); } else { # if we had layered transactions, life would be good # sadly, we have to just go ahead, even if something @@ -405,7 +420,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 => $self->PrincipalId); if ($status) { return (1, $self->loc("That user is now unprivileged")); } else { @@ -571,13 +586,20 @@ sub LoadOrCreateByEmail { my ($val, $message); + my ( $Address, $Name ) = + RT::Interface::Email::ParseAddressFromHeader($email); + $email = $Address; + $self->LoadByEmail($email); $message = $self->loc('User loaded'); unless ($self->Id) { + $self->Load($email); + } + unless($self->Id) { ( $val, $message ) = $self->Create( Name => $email, EmailAddress => $email, - RealName => $email, + RealName => $Name, Privileged => 0, Comments => 'Autocreated when added as a watcher'); unless ($val) { @@ -644,11 +666,13 @@ sub ValidateEmailAddress { -=item CanonicalizeEmailAddress ADDRESS +=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 +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 @@ -667,6 +691,32 @@ sub CanonicalizeEmailAddress { # }}} +# {{{ sub CanonicalizeUserInfo + + + +=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 @@ -687,7 +737,11 @@ sub SetRandomPassword { return ( 0, $self->loc("Permission Denied") ); } - my $pass = $self->GenerateRandomPassword( 6, 8 ); + + my $min = ( $RT::MinimumPasswordLength > 6 ? $RT::MinimumPasswordLength : 6); + my $max = ( $RT::MinimumPasswordLength > 8 ? $RT::MinimumPasswordLength : 8); + + my $pass = $self->GenerateRandomPassword( $min, $max) ; # If we have "notify user on @@ -729,11 +783,11 @@ sub ResetPassword { my $template = RT::Template->new( $self->CurrentUser ); - if ( $self->IsPrivileged ) { + if ( $self->Privileged ) { $template->LoadGlobalTemplate('RT_PasswordChange_Privileged'); } else { - $template->LoadGlobalTemplate('RT_PasswordChange_Privileged'); + $template->LoadGlobalTemplate('RT_PasswordChange_NonPrivileged'); } unless ( $template->Id ) { @@ -752,7 +806,7 @@ sub ResetPassword { Argument => $pass ); - $notification->SetTo( $self->EmailAddress ); + $notification->SetHeader( 'To', $self->EmailAddress ); my ($ret); $ret = $notification->Prepare(); @@ -929,7 +983,7 @@ sub GenerateRandomPassword { my $length = $min_length + int( rand( $max_length - $min_length ) ); - my $char = $self->GenerateRandomNextChar( $total_sum, $start_freq ); + my $char = $self->_GenerateRandomNextChar( $total_sum, $start_freq ); my @word = ( $char + $a ); for ( 2 .. $length ) { $char = @@ -975,25 +1029,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") ); + return ( 0, $self->loc("Password needs to be at least [_1] characters long", $RT::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 -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 @@ -1002,13 +1064,48 @@ sub _GeneratePassword { my $password = shift; my $md5 = Digest::MD5->new(); - $md5->add($password); + $md5->add(encode_utf8($password)); + return ($md5->hexdigest); + +} + +=head2 _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); } # }}} + +=head2 HasPassword + +Returns true if the user has a valid password, otherwise returns false. + +=cut + + +sub HasPassword { + my $self = shift; + my $pwd = $self->__Value('Password'); + return undef if !defined $pwd + || $pwd eq '' + || $pwd eq '*NO-PASSWORD*'; + return 1; +} + + # {{{ sub IsPassword =head2 IsPassword @@ -1035,8 +1132,7 @@ sub IsPassword { return (undef); } - if ( ($self->__Value('Password') eq '') || - ($self->__Value('Password') eq undef) ) { + unless ($self->HasPassword) { return(undef); } @@ -1046,9 +1142,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 @@ -1100,7 +1199,7 @@ The response is cached. PrincipalObj should never ever change. 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"); +is($u->PrincipalObj->PrincipalType, 'User' , "Principal 1 is a user, not a group"); =end testing @@ -1189,9 +1288,29 @@ sub HasGroupRight { # }}} +# {{{ sub OwnGroups + +=head2 OwnGroups + +Returns a group collection object containing the groups of which this +user is a member. + +=cut + +sub OwnGroups { + my $self = shift; + my $groups = RT::Groups->new($self->CurrentUser); + $groups->LimitToUserDefinedGroups; + $groups->WithMember(PrincipalId => $self->Id, + Recursively => 1); + return $groups; +} + +# }}} + # {{{ sub Rights testing -=head2 Rights testing +=head1 Rights testing =begin testing @@ -1206,7 +1325,7 @@ 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'); +my ($id, $msg) = $new_user->Create(Name => 'ACLTest'.$$); ok ($id, "Created a new user for acl test $msg"); @@ -1237,7 +1356,7 @@ ok($tickid, "Created ticket: $tickid"); 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'); +$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"); @@ -1264,7 +1383,7 @@ ok($q_as_system->Id, "Loaded the first 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"); +is($new_tick2->QueueObj->id, $q_as_system->Id, "Created a new ticket in queue 1"); # make sure that the user can't do this without subgroup membership @@ -1272,7 +1391,7 @@ ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User # Create a subgroup my $subgroup = RT::Group->new($RT::SystemUser); -$subgroup->CreateUserDefinedGroup(Name => 'Subgrouptest'); +$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); @@ -1287,8 +1406,8 @@ ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User c # {{{ 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); +($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); @@ -1433,7 +1552,7 @@ ok($rqv, "Revoked the right successfully - $rqm"); # {{{ sub HasRight -=head2 sub HasRight +=head2 HasRight Shim around PrincipalObj->HasRight. See RT::Principal @@ -1492,8 +1611,8 @@ sub CurrentUserCanModify { =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 @@ -1504,6 +1623,216 @@ 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; +} + +# {{{ sub Preferences + +=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; + if (ref ($content) eq 'HASH') { + 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; + } + else { + return defined $content ? $content : $default; + } +} + +# }}} + +# {{{ sub SetPreferences + +=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; + 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; + +} + + +# {{{ sub _CleanupInvalidDelegations + +=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. + +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 @@ -1514,6 +1843,8 @@ sub _Set { my %args = ( Field => undef, Value => undef, + TransactionType => 'Set', + RecordTransaction => 1, @_ ); @@ -1527,13 +1858,29 @@ 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 ); + } } # }}} @@ -1583,6 +1930,14 @@ sub _Value { # }}} +sub BasicColumns { + ( + [ Name => 'User Id' ], + [ EmailAddress => 'Email' ], + [ RealName => 'Name' ], + [ Organization => 'Organization' ], + ); +} 1;