X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FGroupMember.pm;h=5f35a7fb9637374c8139f9ed8b7ab40e08ff47b1;hb=681a340f6be4184b1472a8e1fa9cd5d074f6f325;hp=69de50b42d4778eea8a327d093764b8e7fcaa0f6;hpb=0ebeec96313dd7edfca340f01f8fbbbac1f4aa1d;p=freeside.git diff --git a/rt/lib/RT/GroupMember.pm b/rt/lib/RT/GroupMember.pm index 69de50b42..5f35a7fb9 100755 --- a/rt/lib/RT/GroupMember.pm +++ b/rt/lib/RT/GroupMember.pm @@ -1,6 +1,50 @@ -# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/GroupMember.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $ -# Copyright 2000 Jesse Vincent -# Released under the terms of the GNU Public License +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2016 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 +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# 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: +# +# (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 @@ -8,9 +52,12 @@ =head1 SYNOPSIS -RT::GroupMember should never be called directly. It should generally +RT::GroupMember should never be called directly. It should ONLY only be accessed through the helper functions in RT::Group; +If you're operating on an RT::GroupMember object yourself, you B +doing something wrong. + =head1 DESCRIPTION @@ -19,118 +66,538 @@ only be accessed through the helper functions in RT::Group; =head1 METHODS -=begin testing -ok (require RT::TestHarness); -ok (require RT::GroupMember); -=end testing +=cut + + +package RT::GroupMember; + +use strict; +use warnings; + + +use base 'RT::Record'; + +sub Table {'GroupMembers'} + +use RT::CachedGroupMembers; + + +=head2 Create { Group => undef, Member => undef } + +Add a Principal to the group Group. +if the Principal is a group, automatically inserts all +members of the principal into the cached members table recursively down. + +Both Group and Member are expected to be RT::Principal objects =cut -package RT::GroupMember; -use RT::Record; -use vars qw|@ISA|; -@ISA= qw(RT::Record); - -# {{{ sub _Init -sub _Init { - my $self = shift; - $self->{'table'} = "GroupMembers"; - return($self->SUPER::_Init(@_)); +sub _InsertCGM { + my $self = shift; + + my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser ); + my $cached_id = $cached_member->Create( + Member => $self->MemberObj, + Group => $self->GroupObj, + ImmediateParent => $self->GroupObj, + Via => '0' + ); + + + #When adding a member to a group, we need to go back + #and popuplate the CachedGroupMembers of all the groups that group is part of . + + my $cgm = RT::CachedGroupMembers->new( $self->CurrentUser ); + + # find things which have the current group as a member. + # $group is an RT::Principal for the group. + $cgm->LimitToGroupsWithMember( $self->GroupId ); + $cgm->Limit( + SUBCLAUSE => 'filter', # dont't mess up with prev condition + FIELD => 'MemberId', + OPERATOR => '!=', + VALUE => 'main.GroupId', + QUOTEVALUE => 0, + ENTRYAGGREGATOR => 'AND', + ); + + while ( my $parent_member = $cgm->Next ) { + my $parent_id = $parent_member->MemberId; + my $via = $parent_member->Id; + my $group_id = $parent_member->GroupId; + + my $other_cached_member = + RT::CachedGroupMember->new( $self->CurrentUser ); + my $other_cached_id = $other_cached_member->Create( + Member => $self->MemberObj, + Group => $parent_member->GroupObj, + ImmediateParent => $parent_member->MemberObj, + Via => $parent_member->Id + ); + unless ($other_cached_id) { + $RT::Logger->err( "Couldn't add " . $self->MemberId + . " as a submember of a supergroup" ); + return; + } + } + + return $cached_id; } -# }}} -# {{{ sub _Accessible -sub _Accessible { +sub Create { my $self = shift; - my %Cols = ( - GroupId => 'read', - UserId => 'read' - ); + my %args = ( + Group => undef, + Member => undef, + InsideTransaction => undef, + @_ + ); + + unless ($args{'Group'} && + UNIVERSAL::isa($args{'Group'}, 'RT::Principal') && + $args{'Group'}->Id ) { + + $RT::Logger->warning("GroupMember::Create called with a bogus Group arg"); + return (undef); + } - return $self->SUPER::_Accessible(@_, %Cols); -} -# }}} + unless($args{'Group'}->IsGroup) { + $RT::Logger->warning("Someone tried to add a member to a user instead of a group"); + return (undef); + } -# {{{ sub Create + unless ($args{'Member'} && + UNIVERSAL::isa($args{'Member'}, 'RT::Principal') && + $args{'Member'}->Id) { + $RT::Logger->warning("GroupMember::Create called with a bogus Principal arg"); + return (undef); + } -# a helper method for Add -sub Create { - my $self = shift; - my %args = ( GroupId => undef, - UserId => undef, - @_ - ); - - unless( $self->CurrentUser->HasSystemRight('AdminGroups')) { - return (0, 'Permission Denied'); + #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. + # TODO what about the groups key cache? + RT::Principal->InvalidateACLCache(); + + $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'}); + + # We really need to make sure we don't add any members to this group + # that contain the group itself. that would, um, suck. + # (and recurse infinitely) Later, we can add code to check this in the + # cache and bail so we can support cycling directed graphs + + if ($args{'Member'}->IsGroup) { + my $member_object = $args{'Member'}->Object; + if ($member_object->HasMemberRecursively($args{'Group'})) { + $RT::Logger->debug("Adding that group would create a loop"); + $RT::Handle->Rollback() unless ($args{'InsideTransaction'}); + return(undef); + } + elsif ( $args{'Member'}->Id == $args{'Group'}->Id) { + $RT::Logger->debug("Can't add a group to itself"); + $RT::Handle->Rollback() unless ($args{'InsideTransaction'}); + return(undef); + } + } + + + my $id = $self->SUPER::Create( + GroupId => $args{'Group'}->Id, + MemberId => $args{'Member'}->Id + ); + + unless ($id) { + $RT::Handle->Rollback() unless ($args{'InsideTransaction'}); + return (undef); } - return ($self->SUPER::Create(GroupId => $args{'GroupId'}, - UserId => $args{'UserId'})) + my $clone = RT::GroupMember->new( $self->CurrentUser ); + $clone->Load( $id ); + my $cached_id = $clone->_InsertCGM; + + unless ($cached_id) { + $RT::Handle->Rollback() unless ($args{'InsideTransaction'}); + return (undef); + } + + $RT::Handle->Commit() unless ($args{'InsideTransaction'}); + + return ($id); } -# }}} -# {{{ sub Add -=head2 Add -Takes a paramhash of UserId and GroupId. makes that user a memeber -of that group +=head2 _StashUser PRINCIPAL + +Create { Group => undef, Member => undef } + +Creates an entry in the groupmembers table, which lists a user +as a member of himself. This makes ACL checks a whole bunch easier. +This happens once on user create and never ever gets yanked out. + +PRINCIPAL is expected to be an RT::Principal object for a user + +This routine expects to be called inside a transaction by RT::User->Create =cut -sub Add { +sub _StashUser { my $self = shift; - return ($self->Create(@_)); + my %args = ( + Group => undef, + Member => undef, + @_ + ); + + #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. + # TODO what about the groups key cache? + RT::Principal->InvalidateACLCache(); + + + # We really need to make sure we don't add any members to this group + # that contain the group itself. that would, um, suck. + # (and recurse infinitely) Later, we can add code to check this in the + # cache and bail so we can support cycling directed graphs + + my $id = $self->SUPER::Create( + GroupId => $args{'Group'}->Id, + MemberId => $args{'Member'}->Id, + ); + + unless ($id) { + return (undef); + } + + my $cached_member = RT::CachedGroupMember->new( $self->CurrentUser ); + my $cached_id = $cached_member->Create( + Member => $args{'Member'}, + Group => $args{'Group'}, + ImmediateParent => $args{'Group'}, + Via => '0' + ); + + unless ($cached_id) { + return (undef); + } + + return ($id); } -# }}} -# {{{ sub Delete + =head2 Delete Takes no arguments. deletes the currently loaded member from the group in question. +Expects to be called _outside_ a transaction + =cut sub Delete { my $self = shift; - unless ($self->CurrentUser->HasSystemRight('AdminGroups')) { - return (0, 'Permission Denied'); + + + $RT::Handle->BeginTransaction(); + + # Find all occurrences of this member as a member of this group + # in the cache and nuke them, recursively. + + # The following code will delete all Cached Group members + # where this member's group is _not_ the primary group + # (Ie if we're deleting C as a member of B, and B happens to be + # a member of A, will delete C as a member of A without touching + # C as a member of B + + my $cached_submembers = RT::CachedGroupMembers->new( $self->CurrentUser ); + + $cached_submembers->Limit( + FIELD => 'MemberId', + OPERATOR => '=', + VALUE => $self->MemberObj->Id + ); + + $cached_submembers->Limit( + FIELD => 'ImmediateParentId', + OPERATOR => '=', + VALUE => $self->GroupObj->Id + ); + + + + + + while ( my $item_to_del = $cached_submembers->Next() ) { + my $del_err = $item_to_del->Delete(); + unless ($del_err) { + $RT::Handle->Rollback(); + $RT::Logger->warning("Couldn't delete cached group submember ".$item_to_del->Id); + return (undef); + } + } + + my ($err, $msg) = $self->SUPER::Delete(); + unless ($err) { + $RT::Logger->warning("Couldn't delete cached group submember ".$self->Id); + $RT::Handle->Rollback(); + return (undef); } - return($self->SUPER::Delete(@_)); + + #Clear the key cache. TODO someday we may want to just clear a little bit of the keycache space. + # TODO what about the groups key cache? + RT::Principal->InvalidateACLCache(); + + $RT::Handle->Commit(); + return ($err); + } -# }}} -# {{{ sub UserObj -=head2 UserObj +=head2 MemberObj -Returns an RT::User object for the user specified by $self->UserId +Returns an RT::Principal object for the Principal specified by $self->MemberId =cut -sub UserObj { +sub MemberObj { my $self = shift; - unless (defined ($self->{'user_obj'})) { - $self->{'user_obj'} = new RT::User($self->CurrentUser); - $self->{'user_obj'}->Load($self->UserId); + unless ( defined( $self->{'Member_obj'} ) ) { + $self->{'Member_obj'} = RT::Principal->new( $self->CurrentUser ); + $self->{'Member_obj'}->Load( $self->MemberId ) if ($self->MemberId); } - return($self->{'user_obj'}); + return ( $self->{'Member_obj'} ); +} + + + +=head2 GroupObj + +Returns an RT::Principal object for the Group specified in $self->GroupId + +=cut + +sub GroupObj { + my $self = shift; + unless ( defined( $self->{'Group_obj'} ) ) { + $self->{'Group_obj'} = RT::Principal->new( $self->CurrentUser ); + $self->{'Group_obj'}->Load( $self->GroupId ); + } + return ( $self->{'Group_obj'} ); +} + + + + + + +=head2 id + +Returns the current value of id. +(In the database, id is stored as int(11).) + + +=cut + + +=head2 GroupId + +Returns the current value of GroupId. +(In the database, GroupId is stored as int(11).) + + + +=head2 SetGroupId VALUE + + +Set GroupId to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, GroupId will be stored as a int(11).) + + +=cut + + +=head2 MemberId + +Returns the current value of MemberId. +(In the database, MemberId is stored as int(11).) + + + +=head2 SetMemberId VALUE + + +Set MemberId to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, MemberId will be stored as a int(11).) + + +=cut + + +=head2 Creator + +Returns the current value of Creator. +(In the database, Creator is stored as int(11).) + + +=cut + + +=head2 Created + +Returns the current value of Created. +(In the database, Created is stored as datetime.) + + +=cut + + +=head2 LastUpdatedBy + +Returns the current value of LastUpdatedBy. +(In the database, LastUpdatedBy is stored as int(11).) + + +=cut + + +=head2 LastUpdated + +Returns the current value of LastUpdated. +(In the database, LastUpdated is stored as datetime.) + + +=cut + + + +sub _CoreAccessible { + { + + id => + {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, + GroupId => + {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + MemberId => + {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + Creator => + {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + Created => + {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, + LastUpdatedBy => + {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + LastUpdated => + {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, + + } +}; + +sub FindDependencies { + my $self = shift; + my ($walker, $deps) = @_; + + $self->SUPER::FindDependencies($walker, $deps); + + $deps->Add( out => $self->GroupObj->Object ); + $deps->Add( out => $self->MemberObj->Object ); } -# {{{ sub _Set -sub _Set { +sub __DependsOn { my $self = shift; - unless ($self->CurrentUser->HasSystemRight('AdminGroups')) { - return (0, 'Permission Denied'); + my %args = ( + Shredder => undef, + Dependencies => undef, + @_, + ); + my $deps = $args{'Dependencies'}; + my $list = []; + + my $objs = RT::CachedGroupMembers->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'MemberId', VALUE => $self->MemberId ); + $objs->Limit( FIELD => 'ImmediateParentId', VALUE => $self->GroupId ); + push( @$list, $objs ); + + $deps->_PushDependencies( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON, + TargetObjects => $list, + Shredder => $args{'Shredder'} + ); + + my $group = $self->GroupObj->Object; + # XXX: If we delete member of the ticket owner role group then we should also + # fix ticket object, but only if we don't plan to delete group itself! + unless( ($group->Name || '') eq 'Owner' && + ($group->Domain || '') eq 'RT::Ticket-Role' ) { + return $self->SUPER::__DependsOn( %args ); } - return($self->SUPER::_Set(@_)); + + # we don't delete group, so we have to fix Ticket and Group + $deps->_PushDependencies( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::VARIABLE, + TargetObjects => $group, + Shredder => $args{'Shredder'} + ); + $args{'Shredder'}->PutResolver( + BaseClass => ref $self, + TargetClass => ref $group, + Code => sub { + my %args = (@_); + my $group = $args{'TargetObject'}; + return if $args{'Shredder'}->GetState( Object => $group ) + & (RT::Shredder::Constants::WIPED|RT::Shredder::Constants::IN_WIPING); + return unless ($group->Name || '') eq 'Owner'; + return unless ($group->Domain || '') eq 'RT::Ticket-Role'; + + return if $group->MembersObj->Count > 1; + + my $group_member = $args{'BaseObject'}; + + if( $group_member->MemberObj->id == RT->Nobody->id ) { + RT::Shredder::Exception->throw( "Couldn't delete Nobody from owners role group" ); + } + + my( $status, $msg ) = $group->AddMember( RT->Nobody->id ); + + RT::Shredder::Exception->throw( $msg ) unless $status; + + return; + }, + ); + + return $self->SUPER::__DependsOn( %args ); } -# }}} + +sub PreInflate { + my $class = shift; + my ($importer, $uid, $data) = @_; + + $class->SUPER::PreInflate( $importer, $uid, $data ); + + my $obj = RT::GroupMember->new( RT->SystemUser ); + $obj->LoadByCols( + GroupId => $data->{GroupId}, + MemberId => $data->{MemberId}, + ); + if ($obj->id) { + $importer->Resolve( $uid => ref($obj) => $obj->Id ); + return; + } + + return 1; +} + +sub PostInflate { + my $self = shift; + + $self->_InsertCGM; +} + +RT::Base->_ImportOverlays(); + +1;