summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Group_Overlay.pm
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/Group_Overlay.pm')
-rw-r--r--rt/lib/RT/Group_Overlay.pm1394
1 files changed, 1394 insertions, 0 deletions
diff --git a/rt/lib/RT/Group_Overlay.pm b/rt/lib/RT/Group_Overlay.pm
new file mode 100644
index 000000000..5bf5f7c23
--- /dev/null
+++ b/rt/lib/RT/Group_Overlay.pm
@@ -0,0 +1,1394 @@
+
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+# <jesse@bestpractical.com>
+#
+# (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 }}}
+
+# Released under the terms of version 2 of the GNU Public License
+
+=head1 NAME
+
+ RT::Group - RT\'s group object
+
+=head1 SYNOPSIS
+
+use RT::Group;
+my $group = new RT::Group($CurrentUser);
+
+=head1 DESCRIPTION
+
+An RT group object.
+
+=head1 METHODS
+
+
+
+
+
+=cut
+
+
+package RT::Group;
+
+use strict;
+no warnings qw(redefine);
+
+use RT::Users;
+use RT::GroupMembers;
+use RT::Principals;
+use RT::ACL;
+
+use vars qw/$RIGHTS/;
+
+$RIGHTS = {
+ AdminGroup => 'Modify group metadata or delete group', # loc_pair
+ AdminGroupMembership =>
+ 'Modify membership roster for this group', # loc_pair
+ DelegateRights =>
+ "Delegate specific rights which have been granted to you.", # loc_pair
+ ModifyOwnMembership => 'Join or leave this group', # loc_pair
+ EditSavedSearches => 'Edit saved searches for this group', # loc_pair
+ ShowSavedSearches => 'Display saved searches for this group', # loc_pair
+ SeeGroup => 'Make this group visible to user', # loc_pair
+
+ SeeGroupDashboard => 'View dashboards for this group', #loc_pair
+ CreateGroupDashboard => 'Create dashboards for this group', #loc_pair
+ ModifyGroupDashboard => 'Modify dashboards for this group', #loc_pair
+ DeleteGroupDashboard => 'Delete dashboards for this group', #loc_pair
+};
+
+# Tell RT::ACE that this sort of object can get acls granted
+$RT::ACE::OBJECT_TYPES{'RT::Group'} = 1;
+
+
+#
+
+# TODO: This should be refactored out into an RT::ACLedObject or something
+# stuff the rights into a hash of rights that can exist.
+
+foreach my $right ( keys %{$RIGHTS} ) {
+ $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
+}
+
+=head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
+
+Adds the given rights to the list of possible rights. This method
+should be called during server startup, not at runtime.
+
+=cut
+
+sub AddRights {
+ my $self = shift;
+ my %new = @_;
+ $RIGHTS = { %$RIGHTS, %new };
+ %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
+ map { lc($_) => $_ } keys %new);
+}
+
+=head2 AvailableRights
+
+Returns a hash of available rights for this object. The keys are the right names and the values are a description of what the rights do
+
+=cut
+
+sub AvailableRights {
+ my $self = shift;
+ return($RIGHTS);
+}
+
+
+# {{{ sub SelfDescription
+
+=head2 SelfDescription
+
+Returns a user-readable description of what this group is for and what it's named.
+
+=cut
+
+sub SelfDescription {
+ my $self = shift;
+ if ($self->Domain eq 'ACLEquivalence') {
+ my $user = RT::Principal->new($self->CurrentUser);
+ $user->Load($self->Instance);
+ return $self->loc("user [_1]",$user->Object->Name);
+ }
+ elsif ($self->Domain eq 'UserDefined') {
+ return $self->loc("group '[_1]'",$self->Name);
+ }
+ elsif ($self->Domain eq 'Personal') {
+ my $user = RT::User->new($self->CurrentUser);
+ $user->Load($self->Instance);
+ return $self->loc("personal group '[_1]' for user '[_2]'",$self->Name, $user->Name);
+ }
+ elsif ($self->Domain eq 'RT::System-Role') {
+ return $self->loc("system [_1]",$self->Type);
+ }
+ elsif ($self->Domain eq 'RT::Queue-Role') {
+ my $queue = RT::Queue->new($self->CurrentUser);
+ $queue->Load($self->Instance);
+ return $self->loc("queue [_1] [_2]",$queue->Name, $self->Type);
+ }
+ elsif ($self->Domain eq 'RT::Ticket-Role') {
+ return $self->loc("ticket #[_1] [_2]",$self->Instance, $self->Type);
+ }
+ elsif ($self->Domain eq 'SystemInternal') {
+ return $self->loc("system group '[_1]'",$self->Type);
+ }
+ else {
+ return $self->loc("undescribed group [_1]",$self->Id);
+ }
+}
+
+# }}}
+
+# {{{ sub Load
+
+=head2 Load ID
+
+Load a group object from the database. Takes a single argument.
+If the argument is numerical, load by the column 'id'. Otherwise,
+complain and return.
+
+=cut
+
+sub Load {
+ my $self = shift;
+ my $identifier = shift || return undef;
+
+ if ( $identifier !~ /\D/ ) {
+ $self->SUPER::LoadById($identifier);
+ }
+ else {
+ $RT::Logger->crit("Group -> Load called with a bogus argument");
+ return undef;
+ }
+}
+
+# }}}
+
+# {{{ sub LoadUserDefinedGroup
+
+=head2 LoadUserDefinedGroup NAME
+
+Loads a system group from the database. The only argument is
+the group's name.
+
+
+=cut
+
+sub LoadUserDefinedGroup {
+ my $self = shift;
+ my $identifier = shift;
+
+ if ( $identifier =~ /^\d+$/ ) {
+ return $self->LoadByCols(
+ Domain => 'UserDefined',
+ id => $identifier,
+ );
+ } else {
+ return $self->LoadByCols(
+ Domain => 'UserDefined',
+ Name => $identifier,
+ );
+ }
+}
+
+# }}}
+
+# {{{ sub LoadACLEquivalenceGroup
+
+=head2 LoadACLEquivalenceGroup PRINCIPAL
+
+Loads a user's acl equivalence group. Takes a principal object or its ID.
+ACL equivalnce groups are used to simplify the acl system. Each user
+has one group that only he is a member of. Rights granted to the user
+are actually granted to that group. This greatly simplifies ACL checks.
+While this results in a somewhat more complex setup when creating users
+and granting ACLs, it _greatly_ simplifies acl checks.
+
+=cut
+
+sub LoadACLEquivalenceGroup {
+ my $self = shift;
+ my $principal = shift;
+ $principal = $principal->id if ref $principal;
+
+ return $self->LoadByCols(
+ Domain => 'ACLEquivalence',
+ Type => 'UserEquiv',
+ Instance => $principal,
+ );
+}
+
+# }}}
+
+# {{{ sub LoadPersonalGroup
+
+=head2 LoadPersonalGroup {Name => NAME, User => USERID}
+
+Loads a personal group from the database.
+
+=cut
+
+sub LoadPersonalGroup {
+ my $self = shift;
+ my %args = ( Name => undef,
+ User => undef,
+ @_);
+
+ $self->LoadByCols( "Domain" => 'Personal',
+ "Instance" => $args{'User'},
+ "Type" => '',
+ "Name" => $args{'Name'} );
+}
+
+# }}}
+
+# {{{ sub LoadSystemInternalGroup
+
+=head2 LoadSystemInternalGroup NAME
+
+Loads a Pseudo group from the database. The only argument is
+the group's name.
+
+
+=cut
+
+sub LoadSystemInternalGroup {
+ my $self = shift;
+ my $identifier = shift;
+
+ return $self->LoadByCols(
+ Domain => 'SystemInternal',
+ Type => $identifier,
+ );
+}
+
+# }}}
+
+# {{{ sub LoadTicketRoleGroup
+
+=head2 LoadTicketRoleGroup { Ticket => TICKET_ID, Type => TYPE }
+
+Loads a ticket group from the database.
+
+Takes a param hash with 2 parameters:
+
+ Ticket is the TicketId we're curious about
+ Type is the type of Group we're trying to load:
+ Requestor, Cc, AdminCc, Owner
+
+=cut
+
+sub LoadTicketRoleGroup {
+ my $self = shift;
+ my %args = (Ticket => '0',
+ Type => undef,
+ @_);
+ $self->LoadByCols( Domain => 'RT::Ticket-Role',
+ Instance =>$args{'Ticket'},
+ Type => $args{'Type'}
+ );
+}
+
+# }}}
+
+# {{{ sub LoadQueueRoleGroup
+
+=head2 LoadQueueRoleGroup { Queue => Queue_ID, Type => TYPE }
+
+Loads a Queue group from the database.
+
+Takes a param hash with 2 parameters:
+
+ Queue is the QueueId we're curious about
+ Type is the type of Group we're trying to load:
+ Requestor, Cc, AdminCc, Owner
+
+=cut
+
+sub LoadQueueRoleGroup {
+ my $self = shift;
+ my %args = (Queue => undef,
+ Type => undef,
+ @_);
+ $self->LoadByCols( Domain => 'RT::Queue-Role',
+ Instance =>$args{'Queue'},
+ Type => $args{'Type'}
+ );
+}
+
+# }}}
+
+# {{{ sub LoadSystemRoleGroup
+
+=head2 LoadSystemRoleGroup Type
+
+Loads a System group from the database.
+
+Takes a single param: Type
+
+ Type is the type of Group we're trying to load:
+ Requestor, Cc, AdminCc, Owner
+
+=cut
+
+sub LoadSystemRoleGroup {
+ my $self = shift;
+ my $type = shift;
+ $self->LoadByCols( Domain => 'RT::System-Role',
+ Type => $type
+ );
+}
+
+# }}}
+
+# {{{ sub Create
+
+=head2 Create
+
+You need to specify what sort of group you're creating by calling one of the other
+Create_____ routines.
+
+=cut
+
+sub Create {
+ my $self = shift;
+ $RT::Logger->crit("Someone called RT::Group->Create. this method does not exist. someone's being evil");
+ return(0,$self->loc('Permission Denied'));
+}
+
+# }}}
+
+# {{{ sub _Create
+
+=head2 _Create
+
+Takes a paramhash with named arguments: Name, Description.
+
+Returns a tuple of (Id, Message). If id is 0, the create failed
+
+=cut
+
+sub _Create {
+ my $self = shift;
+ my %args = (
+ Name => undef,
+ Description => undef,
+ Domain => undef,
+ Type => undef,
+ Instance => '0',
+ InsideTransaction => undef,
+ _RecordTransaction => 1,
+ @_
+ );
+
+ $RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
+ # Groups deal with principal ids, rather than user ids.
+ # When creating this group, set up a principal Id for it.
+ my $principal = RT::Principal->new( $self->CurrentUser );
+ my $principal_id = $principal->Create(
+ PrincipalType => 'Group',
+ ObjectId => '0'
+ );
+ $principal->__Set(Field => 'ObjectId', Value => $principal_id);
+
+
+ $self->SUPER::Create(
+ id => $principal_id,
+ Name => $args{'Name'},
+ Description => $args{'Description'},
+ Type => $args{'Type'},
+ Domain => $args{'Domain'},
+ Instance => ($args{'Instance'} || '0')
+ );
+ my $id = $self->Id;
+ unless ($id) {
+ $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
+ return ( 0, $self->loc('Could not create group') );
+ }
+
+ # If we couldn't create a principal Id, get the fuck out.
+ unless ($principal_id) {
+ $RT::Handle->Rollback() unless ($args{'InsideTransaction'});
+ $RT::Logger->crit( "Couldn't create a Principal on new user create. Strange things are afoot at the circle K" );
+ return ( 0, $self->loc('Could not create group') );
+ }
+
+ # Now we make the group a member of itself as a cached group member
+ # this needs to exist so that group ACL checks don't fall over.
+ # you're checking CachedGroupMembers to see if the principal in question
+ # is a member of the principal the rights have been granted too
+
+ # in the ordinary case, this would fail badly because it would recurse and add all the members of this group as
+ # cached members. thankfully, we're creating the group now...so it has no members.
+ my $cgm = RT::CachedGroupMember->new($self->CurrentUser);
+ $cgm->Create(Group =>$self->PrincipalObj, Member => $self->PrincipalObj, ImmediateParent => $self->PrincipalObj);
+
+
+ if ( $args{'_RecordTransaction'} ) {
+ $self->_NewTransaction( Type => "Create" );
+ }
+
+ $RT::Handle->Commit() unless ($args{'InsideTransaction'});
+
+ return ( $id, $self->loc("Group created") );
+}
+
+# }}}
+
+# {{{ CreateUserDefinedGroup
+
+=head2 CreateUserDefinedGroup { Name => "name", Description => "Description"}
+
+A helper subroutine which creates a system group
+
+Returns a tuple of (Id, Message). If id is 0, the create failed
+
+=cut
+
+sub CreateUserDefinedGroup {
+ my $self = shift;
+
+ unless ( $self->CurrentUserHasRight('AdminGroup') ) {
+ $RT::Logger->warning( $self->CurrentUser->Name
+ . " Tried to create a group without permission." );
+ return ( 0, $self->loc('Permission Denied') );
+ }
+
+ return($self->_Create( Domain => 'UserDefined', Type => '', Instance => '', @_));
+}
+
+# }}}
+
+# {{{ _CreateACLEquivalenceGroup
+
+=head2 _CreateACLEquivalenceGroup { Principal }
+
+A helper subroutine which creates a group containing only
+an individual user. This gets used by the ACL system to check rights.
+Yes, it denormalizes the data, but that's ok, as we totally win on performance.
+
+Returns a tuple of (Id, Message). If id is 0, the create failed
+
+=cut
+
+sub _CreateACLEquivalenceGroup {
+ my $self = shift;
+ my $princ = shift;
+
+ my $id = $self->_Create( Domain => 'ACLEquivalence',
+ Type => 'UserEquiv',
+ Name => 'User '. $princ->Object->Id,
+ Description => 'ACL equiv. for user '.$princ->Object->Id,
+ Instance => $princ->Id,
+ InsideTransaction => 1);
+ unless ($id) {
+ $RT::Logger->crit("Couldn't create ACL equivalence group");
+ return undef;
+ }
+
+ # We use stashuser so we don't get transactions inside transactions
+ # and so we bypass all sorts of cruft we don't need
+ my $aclstash = RT::GroupMember->new($self->CurrentUser);
+ my ($stash_id, $add_msg) = $aclstash->_StashUser(Group => $self->PrincipalObj,
+ Member => $princ);
+
+ unless ($stash_id) {
+ $RT::Logger->crit("Couldn't add the user to his own acl equivalence group:".$add_msg);
+ # We call super delete so we don't get acl checked.
+ $self->SUPER::Delete();
+ return(undef);
+ }
+ return ($id);
+}
+
+# }}}
+
+# {{{ CreatePersonalGroup
+
+=head2 CreatePersonalGroup { PrincipalId => PRINCIPAL_ID, Name => "name", Description => "Description"}
+
+A helper subroutine which creates a personal group. Generally,
+personal groups are used for ACL delegation and adding to ticket roles
+PrincipalId defaults to the current user's principal id.
+
+Returns a tuple of (Id, Message). If id is 0, the create failed
+
+=cut
+
+sub CreatePersonalGroup {
+ my $self = shift;
+ my %args = (
+ Name => undef,
+ Description => undef,
+ PrincipalId => $self->CurrentUser->PrincipalId,
+ @_
+ );
+
+ if ( $self->CurrentUser->PrincipalId == $args{'PrincipalId'} ) {
+
+ unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups') ) {
+ $RT::Logger->warning( $self->CurrentUser->Name
+ . " Tried to create a group without permission." );
+ return ( 0, $self->loc('Permission Denied') );
+ }
+
+ }
+ else {
+ unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
+ $RT::Logger->warning( $self->CurrentUser->Name
+ . " Tried to create a group without permission." );
+ return ( 0, $self->loc('Permission Denied') );
+ }
+
+ }
+
+ return (
+ $self->_Create(
+ Domain => 'Personal',
+ Type => '',
+ Instance => $args{'PrincipalId'},
+ Name => $args{'Name'},
+ Description => $args{'Description'}
+ )
+ );
+}
+
+# }}}
+
+# {{{ CreateRoleGroup
+
+=head2 CreateRoleGroup { Domain => DOMAIN, Type => TYPE, Instance => ID }
+
+A helper subroutine which creates a ticket group. (What RT 2.0 called Ticket watchers)
+Type is one of ( "Requestor" || "Cc" || "AdminCc" || "Owner")
+Domain is one of (RT::Ticket-Role || RT::Queue-Role || RT::System-Role)
+Instance is the id of the ticket or queue in question
+
+This routine expects to be called from {Ticket||Queue}->CreateTicketGroups _inside of a transaction_
+
+Returns a tuple of (Id, Message). If id is 0, the create failed
+
+=cut
+
+sub CreateRoleGroup {
+ my $self = shift;
+ my %args = ( Instance => undef,
+ Type => undef,
+ Domain => undef,
+ @_ );
+ unless ( $args{'Type'} =~ /^(?:Cc|AdminCc|Requestor|Owner)$/ ) {
+ return ( 0, $self->loc("Invalid Group Type") );
+ }
+
+
+ return ( $self->_Create( Domain => $args{'Domain'},
+ Instance => $args{'Instance'},
+ Type => $args{'Type'},
+ InsideTransaction => 1 ) );
+}
+
+# }}}
+
+# {{{ sub Delete
+
+=head2 Delete
+
+Delete this object
+
+=cut
+
+sub Delete {
+ my $self = shift;
+
+ unless ( $self->CurrentUserHasRight('AdminGroup') ) {
+ return ( 0, 'Permission Denied' );
+ }
+
+ $RT::Logger->crit("Deleting groups violates referential integrity until we go through and fix this");
+ # TODO XXX
+
+ # Remove the principal object
+ # Remove this group from anything it's a member of.
+ # Remove all cached members of this group
+ # Remove any rights granted to this group
+ # remove any rights delegated by way of this group
+
+ return ( $self->SUPER::Delete(@_) );
+}
+
+# }}}
+
+=head2 SetDisabled BOOL
+
+If passed a positive value, this group will be disabled. No rights it commutes or grants will be honored.
+It will not appear in most group listings.
+
+This routine finds all the cached group members that are members of this group (recursively) and disables them.
+
+=cut
+
+ # }}}
+
+ sub SetDisabled {
+ my $self = shift;
+ my $val = shift;
+ if ($self->Domain eq 'Personal') {
+ if ($self->CurrentUser->PrincipalId == $self->Instance) {
+ unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ } else {
+ unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+ }
+ else {
+ unless ( $self->CurrentUserHasRight('AdminGroup') ) {
+ return (0, $self->loc('Permission Denied'));
+ }
+ }
+ $RT::Handle->BeginTransaction();
+ $self->PrincipalObj->SetDisabled($val);
+
+
+
+
+ # 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 => 'ImmediateParentId', OPERATOR => '=', VALUE => $self->Id);
+
+ #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();
+
+
+
+ while ( my $item = $cached_submembers->Next() ) {
+ my $del_err = $item->SetDisabled($val);
+ unless ($del_err) {
+ $RT::Handle->Rollback();
+ $RT::Logger->warning("Couldn't disable cached group submember ".$item->Id);
+ return (undef);
+ }
+ }
+
+ $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
+
+ $RT::Handle->Commit();
+ if ( $val == 1 ) {
+ return (1, $self->loc("Group disabled"));
+ } else {
+ return (1, $self->loc("Group enabled"));
+ }
+
+}
+
+# }}}
+
+
+
+sub Disabled {
+ my $self = shift;
+ $self->PrincipalObj->Disabled(@_);
+}
+
+
+# {{{ DeepMembersObj
+
+=head2 DeepMembersObj
+
+Returns an RT::CachedGroupMembers object of this group's members,
+including all members of subgroups.
+
+=cut
+
+sub DeepMembersObj {
+ my $self = shift;
+ my $members_obj = RT::CachedGroupMembers->new( $self->CurrentUser );
+
+ #If we don't have rights, don't include any results
+ # TODO XXX WHY IS THERE NO ACL CHECK HERE?
+ $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
+
+ return ( $members_obj );
+
+}
+
+# }}}
+
+# {{{ MembersObj
+
+=head2 MembersObj
+
+Returns an RT::GroupMembers object of this group's direct members.
+
+=cut
+
+sub MembersObj {
+ my $self = shift;
+ my $members_obj = RT::GroupMembers->new( $self->CurrentUser );
+
+ #If we don't have rights, don't include any results
+ # TODO XXX WHY IS THERE NO ACL CHECK HERE?
+ $members_obj->LimitToMembersOfGroup( $self->PrincipalId );
+
+ return ( $members_obj );
+
+}
+
+# }}}
+
+# {{{ GroupMembersObj
+
+=head2 GroupMembersObj [Recursively => 1]
+
+Returns an L<RT::Groups> object of this group's members.
+By default returns groups including all subgroups, but
+could be changed with C<Recursively> named argument.
+
+B<Note> that groups are not filtered by type and result
+may contain as well system groups, personal and other.
+
+=cut
+
+sub GroupMembersObj {
+ my $self = shift;
+ my %args = ( Recursively => 1, @_ );
+
+ my $groups = RT::Groups->new( $self->CurrentUser );
+ my $members_table = $args{'Recursively'}?
+ 'CachedGroupMembers': 'GroupMembers';
+
+ my $members_alias = $groups->NewAlias( $members_table );
+ $groups->Join(
+ ALIAS1 => $members_alias, FIELD1 => 'MemberId',
+ ALIAS2 => $groups->PrincipalsAlias, FIELD2 => 'id',
+ );
+ $groups->Limit(
+ ALIAS => $members_alias,
+ FIELD => 'GroupId',
+ VALUE => $self->PrincipalId,
+ );
+ $groups->Limit(
+ ALIAS => $members_alias,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ ) if $args{'Recursively'};
+
+ return $groups;
+}
+
+# }}}
+
+# {{{ UserMembersObj
+
+=head2 UserMembersObj
+
+Returns an L<RT::Users> object of this group's members, by default
+returns users including all members of subgroups, but could be
+changed with C<Recursively> named argument.
+
+=cut
+
+sub UserMembersObj {
+ my $self = shift;
+ my %args = ( Recursively => 1, @_ );
+
+ #If we don't have rights, don't include any results
+ # TODO XXX WHY IS THERE NO ACL CHECK HERE?
+
+ my $members_table = $args{'Recursively'}?
+ 'CachedGroupMembers': 'GroupMembers';
+
+ my $users = RT::Users->new($self->CurrentUser);
+ my $members_alias = $users->NewAlias( $members_table );
+ $users->Join(
+ ALIAS1 => $members_alias, FIELD1 => 'MemberId',
+ ALIAS2 => $users->PrincipalsAlias, FIELD2 => 'id',
+ );
+ $users->Limit(
+ ALIAS => $members_alias,
+ FIELD => 'GroupId',
+ VALUE => $self->PrincipalId,
+ );
+ $users->Limit(
+ ALIAS => $members_alias,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ ) if $args{'Recursively'};
+
+ return ( $users);
+}
+
+# }}}
+
+# {{{ MemberEmailAddresses
+
+=head2 MemberEmailAddresses
+
+Returns an array of the email addresses of all of this group's members
+
+
+=cut
+
+sub MemberEmailAddresses {
+ my $self = shift;
+
+ my %addresses;
+ my $members = $self->UserMembersObj();
+ while (my $member = $members->Next) {
+ $addresses{$member->EmailAddress} = 1;
+ }
+ return(sort keys %addresses);
+}
+
+# }}}
+
+# {{{ MemberEmailAddressesAsString
+
+=head2 MemberEmailAddressesAsString
+
+Returns a comma delimited string of the email addresses of all users
+who are members of this group.
+
+=cut
+
+
+sub MemberEmailAddressesAsString {
+ my $self = shift;
+ return (join(', ', $self->MemberEmailAddresses));
+}
+
+# }}}
+
+# {{{ AddMember
+
+=head2 AddMember PRINCIPAL_ID
+
+AddMember adds a principal to this group. It takes a single principal id.
+Returns a two value array. the first value is true on successful
+addition or 0 on failure. The second value is a textual status msg.
+
+=cut
+
+sub AddMember {
+ my $self = shift;
+ my $new_member = shift;
+
+
+
+ if ($self->Domain eq 'Personal') {
+ if ($self->CurrentUser->PrincipalId == $self->Instance) {
+ unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ } else {
+ unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+ }
+
+ else {
+ # We should only allow membership changes if the user has the right
+ # to modify group membership or the user is the principal in question
+ # and the user has the right to modify his own membership
+ unless ( ($new_member == $self->CurrentUser->PrincipalId &&
+ $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
+ $self->CurrentUserHasRight('AdminGroupMembership') ) {
+ #User has no permission to be doing this
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ }
+ $self->_AddMember(PrincipalId => $new_member);
+}
+
+# A helper subroutine for AddMember that bypasses the ACL checks
+# this should _ONLY_ ever be called from Ticket/Queue AddWatcher
+# when we want to deal with groups according to queue rights
+# In the dim future, this will all get factored out and life
+# will get better
+
+# takes a paramhash of { PrincipalId => undef, InsideTransaction }
+
+sub _AddMember {
+ my $self = shift;
+ my %args = ( PrincipalId => undef,
+ InsideTransaction => undef,
+ @_);
+ my $new_member = $args{'PrincipalId'};
+
+ unless ($self->Id) {
+ $RT::Logger->crit("Attempting to add a member to a group which wasn't loaded. 'oops'");
+ return(0, $self->loc("Group not found"));
+ }
+
+ unless ($new_member =~ /^\d+$/) {
+ $RT::Logger->crit("_AddMember called with a parameter that's not an integer.");
+ }
+
+
+ my $new_member_obj = RT::Principal->new( $self->CurrentUser );
+ $new_member_obj->Load($new_member);
+
+
+ unless ( $new_member_obj->Id ) {
+ $RT::Logger->debug("Couldn't find that principal");
+ return ( 0, $self->loc("Couldn't find that principal") );
+ }
+
+ if ( $self->HasMember( $new_member_obj ) ) {
+
+ #User is already a member of this group. no need to add it
+ return ( 0, $self->loc("Group already has member: [_1]", $new_member_obj->Object->Name) );
+ }
+ if ( $new_member_obj->IsGroup &&
+ $new_member_obj->Object->HasMemberRecursively($self->PrincipalObj) ) {
+
+ #This group can't be made to be a member of itself
+ return ( 0, $self->loc("Groups can't be members of their members"));
+ }
+
+
+ my $member_object = RT::GroupMember->new( $self->CurrentUser );
+ my $id = $member_object->Create(
+ Member => $new_member_obj,
+ Group => $self->PrincipalObj,
+ InsideTransaction => $args{'InsideTransaction'}
+ );
+ if ($id) {
+ return ( 1, $self->loc("Member added: [_1]", $new_member_obj->Object->Name) );
+ }
+ else {
+ return(0, $self->loc("Couldn't add member to group"));
+ }
+}
+# }}}
+
+# {{{ HasMember
+
+=head2 HasMember RT::Principal|id
+
+Takes an L<RT::Principal> object or its id returns a GroupMember Id if that user is a
+member of this group.
+Returns undef if the user isn't a member of the group or if the current
+user doesn't have permission to find out. Arguably, it should differentiate
+between ACL failure and non membership.
+
+=cut
+
+sub HasMember {
+ my $self = shift;
+ my $principal = shift;
+
+ my $id;
+ if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
+ $id = $principal->id;
+ } elsif ( $principal =~ /^\d+$/ ) {
+ $id = $principal;
+ } else {
+ $RT::Logger->error("Group::HasMember was called with an argument that".
+ " isn't an RT::Principal or id. It's ".($principal||'(undefined)'));
+ return(undef);
+ }
+ return undef unless $id;
+
+ my $member_obj = RT::GroupMember->new( $self->CurrentUser );
+ $member_obj->LoadByCols(
+ MemberId => $id,
+ GroupId => $self->PrincipalId
+ );
+
+ if ( my $member_id = $member_obj->id ) {
+ return $member_id;
+ }
+ else {
+ return (undef);
+ }
+}
+
+# }}}
+
+# {{{ HasMemberRecursively
+
+=head2 HasMemberRecursively RT::Principal|id
+
+Takes an L<RT::Principal> object or its id and returns true if that user is a member of
+this group.
+Returns undef if the user isn't a member of the group or if the current
+user doesn't have permission to find out. Arguably, it should differentiate
+between ACL failure and non membership.
+
+=cut
+
+sub HasMemberRecursively {
+ my $self = shift;
+ my $principal = shift;
+
+ my $id;
+ if ( UNIVERSAL::isa($principal,'RT::Principal') ) {
+ $id = $principal->id;
+ } elsif ( $principal =~ /^\d+$/ ) {
+ $id = $principal;
+ } else {
+ $RT::Logger->error("Group::HasMemberRecursively was called with an argument that".
+ " isn't an RT::Principal or id. It's $principal");
+ return(undef);
+ }
+ return undef unless $id;
+
+ my $member_obj = RT::CachedGroupMember->new( $self->CurrentUser );
+ $member_obj->LoadByCols(
+ MemberId => $id,
+ GroupId => $self->PrincipalId
+ );
+
+ if ( my $member_id = $member_obj->id ) {
+ return $member_id;
+ }
+ else {
+ return (undef);
+ }
+}
+
+# }}}
+
+# {{{ DeleteMember
+
+=head2 DeleteMember PRINCIPAL_ID
+
+Takes the principal id of a current user or group.
+If the current user has apropriate rights,
+removes that GroupMember from this group.
+Returns a two value array. the first value is true on successful
+addition or 0 on failure. The second value is a textual status msg.
+
+=cut
+
+sub DeleteMember {
+ my $self = shift;
+ my $member_id = shift;
+
+
+ # We should only allow membership changes if the user has the right
+ # to modify group membership or the user is the principal in question
+ # and the user has the right to modify his own membership
+
+ if ($self->Domain eq 'Personal') {
+ if ($self->CurrentUser->PrincipalId == $self->Instance) {
+ unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ } else {
+ unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+ }
+ else {
+ unless ( (($member_id == $self->CurrentUser->PrincipalId) &&
+ $self->CurrentUserHasRight('ModifyOwnMembership') ) ||
+ $self->CurrentUserHasRight('AdminGroupMembership') ) {
+ #User has no permission to be doing this
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+ $self->_DeleteMember($member_id);
+}
+
+# A helper subroutine for DeleteMember that bypasses the ACL checks
+# this should _ONLY_ ever be called from Ticket/Queue DeleteWatcher
+# when we want to deal with groups according to queue rights
+# In the dim future, this will all get factored out and life
+# will get better
+
+sub _DeleteMember {
+ my $self = shift;
+ my $member_id = shift;
+
+ my $member_obj = RT::GroupMember->new( $self->CurrentUser );
+
+ $member_obj->LoadByCols( MemberId => $member_id,
+ GroupId => $self->PrincipalId);
+
+
+ #If we couldn't load it, return undef.
+ unless ( $member_obj->Id() ) {
+ $RT::Logger->debug("Group has no member with that id");
+ return ( 0,$self->loc( "Group has no such member" ));
+ }
+
+ #Now that we've checked ACLs and sanity, delete the groupmember
+ my $val = $member_obj->Delete();
+
+ if ($val) {
+ return ( $val, $self->loc("Member deleted") );
+ }
+ else {
+ $RT::Logger->debug("Failed to delete group ".$self->Id." member ". $member_id);
+ return ( 0, $self->loc("Member not deleted" ));
+ }
+}
+
+# }}}
+
+# {{{ sub _CleanupInvalidDelegations
+
+=head2 _CleanupInvalidDelegations { InsideTransaction => undef }
+
+Revokes all ACE entries delegated by members of this group 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("Group not loaded.");
+ return (undef);
+ }
+
+ my $in_trans = $args{InsideTransaction};
+
+ # TODO: Can this be unrolled such that the number of DB queries is constant rather than linear in exploded group size?
+ my $members = $self->DeepMembersObj();
+ $members->LimitToUsers();
+ $RT::Handle->BeginTransaction() unless $in_trans;
+ while ( my $member = $members->Next()) {
+ my $ret = $member->MemberObj->_CleanupInvalidDelegations(InsideTransaction => 1,
+ Object => $args{Object});
+ unless ($ret) {
+ $RT::Handle->Rollback() unless $in_trans;
+ return (undef);
+ }
+ }
+ $RT::Handle->Commit() unless $in_trans;
+ return(1);
+}
+
+# }}}
+
+# {{{ ACL Related routines
+
+# {{{ sub _Set
+sub _Set {
+ my $self = shift;
+ my %args = (
+ Field => undef,
+ Value => undef,
+ TransactionType => 'Set',
+ RecordTransaction => 1,
+ @_
+ );
+
+ if ($self->Domain eq 'Personal') {
+ if ($self->CurrentUser->PrincipalId == $self->Instance) {
+ unless ( $self->CurrentUserHasRight('AdminOwnPersonalGroups')) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ } else {
+ unless ( $self->CurrentUserHasRight('AdminAllPersonalGroups') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+ }
+ else {
+ unless ( $self->CurrentUserHasRight('AdminGroup') ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+ }
+
+ 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->Description );
+ }
+ else {
+ return ( $ret, $msg );
+ }
+}
+
+# }}}
+
+
+
+
+=head2 CurrentUserHasRight RIGHTNAME
+
+Returns true if the current user has the specified right for this group.
+
+
+ TODO: we don't deal with membership visibility yet
+
+=cut
+
+
+sub CurrentUserHasRight {
+ my $self = shift;
+ my $right = shift;
+
+
+
+ if ($self->Id &&
+ $self->CurrentUser->HasRight( Object => $self,
+ Right => $right )) {
+ return(1);
+ }
+ elsif ( $self->CurrentUser->HasRight(Object => $RT::System, Right => $right )) {
+ return (1);
+ } else {
+ return(undef);
+ }
+
+}
+
+# }}}
+
+
+
+
+# {{{ 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.
+
+
+=cut
+
+
+sub PrincipalObj {
+ my $self = shift;
+ unless ( defined $self->{'PrincipalObj'} &&
+ defined $self->{'PrincipalObj'}->ObjectId &&
+ ($self->{'PrincipalObj'}->ObjectId == $self->Id) &&
+ (defined $self->{'PrincipalObj'}->PrincipalType &&
+ $self->{'PrincipalObj'}->PrincipalType eq 'Group')) {
+
+ $self->{'PrincipalObj'} = RT::Principal->new($self->CurrentUser);
+ $self->{'PrincipalObj'}->LoadByCols('ObjectId' => $self->Id,
+ 'PrincipalType' => 'Group') ;
+ }
+ return($self->{'PrincipalObj'});
+}
+
+
+=head2 PrincipalId
+
+Returns this user's PrincipalId
+
+=cut
+
+sub PrincipalId {
+ my $self = shift;
+ return $self->Id;
+}
+
+# }}}
+
+sub BasicColumns {
+ (
+ [ Name => 'Name' ],
+ [ Description => 'Description' ],
+ );
+}
+
+1;
+
+=head1 AUTHOR
+
+Jesse Vincent, jesse@bestpractical.com
+
+=head1 SEE ALSO
+
+RT
+