Upgrade to 4.2.8
[usit-rt.git] / lib / RT / Users.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::Users - Collection of RT::User objects
52
53 =head1 SYNOPSIS
54
55   use RT::Users;
56
57
58 =head1 DESCRIPTION
59
60
61 =head1 METHODS
62
63
64 =cut
65
66
67 package RT::Users;
68
69 use strict;
70 use warnings;
71
72 use base 'RT::SearchBuilder';
73
74 use RT::User;
75
76 sub Table { 'Users'}
77
78
79 sub _Init {
80     my $self = shift;
81     $self->{'with_disabled_column'} = 1;
82
83     my @result = $self->SUPER::_Init(@_);
84     # By default, order by name
85     $self->OrderBy( ALIAS => 'main',
86                     FIELD => 'Name',
87                     ORDER => 'ASC' );
88
89     # XXX: should be generalized
90     $self->{'princalias'} = $self->Join(
91                  ALIAS1 => 'main',
92                  FIELD1 => 'id',
93                  TABLE2 => 'Principals',
94                  FIELD2 => 'id' );
95     $self->Limit( ALIAS => $self->{'princalias'},
96                   FIELD => 'PrincipalType',
97                   VALUE => 'User',
98                 );
99
100     return (@result);
101 }
102
103
104 =head2 PrincipalsAlias
105
106 Returns the string that represents this Users object's primary "Principals" alias.
107
108 =cut
109
110 # XXX: should be generalized
111 sub PrincipalsAlias {
112     my $self = shift;
113     return($self->{'princalias'});
114
115 }
116
117
118 =head2 LimitToEnabled
119
120 Only find items that haven't been disabled
121
122 =cut
123
124 # XXX: should be generalized
125 sub LimitToEnabled {
126     my $self = shift;
127
128     $self->{'handled_disabled_column'} = 1;
129     $self->Limit(
130         ALIAS    => $self->PrincipalsAlias,
131         FIELD    => 'Disabled',
132         VALUE    => '0',
133     );
134 }
135
136 =head2 LimitToDeleted
137
138 Only find items that have been deleted.
139
140 =cut
141
142 sub LimitToDeleted {
143     my $self = shift;
144     
145     $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1;
146     $self->Limit(
147         ALIAS => $self->PrincipalsAlias,
148         FIELD => 'Disabled',
149         VALUE => 1,
150     );
151 }
152
153
154
155 =head2 LimitToEmail
156
157 Takes one argument. an email address. limits the returned set to
158 that email address
159
160 =cut
161
162 sub LimitToEmail {
163     my $self = shift;
164     my $addr = shift;
165     $self->Limit( FIELD => 'EmailAddress', VALUE => $addr, CASESENSITIVE => 0 );
166 }
167
168
169
170 =head2 MemberOfGroup PRINCIPAL_ID
171
172 takes one argument, a group's principal id. Limits the returned set
173 to members of a given group
174
175 =cut
176
177 sub MemberOfGroup {
178     my $self  = shift;
179     my $group = shift;
180
181     return $self->loc("No group specified") if ( !defined $group );
182
183     my $groupalias = $self->NewAlias('CachedGroupMembers');
184
185     # Join the principal to the groups table
186     $self->Join( ALIAS1 => $self->PrincipalsAlias,
187                  FIELD1 => 'id',
188                  ALIAS2 => $groupalias,
189                  FIELD2 => 'MemberId' );
190     $self->Limit( ALIAS => $groupalias,
191                   FIELD => 'Disabled',
192                   VALUE => 0 );
193
194     $self->Limit( ALIAS    => "$groupalias",
195                   FIELD    => 'GroupId',
196                   VALUE    => "$group",
197                   OPERATOR => "=" );
198 }
199
200
201
202 =head2 LimitToPrivileged
203
204 Limits to users who can be made members of ACLs and groups
205
206 =cut
207
208 sub LimitToPrivileged {
209     my $self = shift;
210     $self->MemberOfGroup( RT->PrivilegedUsers->id );
211 }
212
213 =head2 LimitToUnprivileged
214
215 Limits to unprivileged users only
216
217 =cut
218
219 sub LimitToUnprivileged {
220     my $self = shift;
221     $self->MemberOfGroup( RT->UnprivilegedUsers->id);
222 }
223
224
225 sub Limit {
226     my $self = shift;
227     my %args = @_;
228     $args{'CASESENSITIVE'} = 0 unless exists $args{'CASESENSITIVE'} or $args{'ALIAS'};
229     return $self->SUPER::Limit( %args );
230 }
231
232 =head2 WhoHaveRight { Right => 'name', Object => $rt_object , IncludeSuperusers => undef, IncludeSubgroupMembers => undef, IncludeSystemRights => undef, EquivObjects => [ ] }
233
234
235 find all users who the right Right for this group, either individually
236 or as members of groups
237
238 If passed a queue object, with no id, it will find users who have that right for _any_ queue
239
240 =cut
241
242 # XXX: should be generalized
243 sub _JoinGroupMembers
244 {
245     my $self = shift;
246     my %args = (
247         IncludeSubgroupMembers => 1,
248         @_
249     );
250
251     my $principals = $self->PrincipalsAlias;
252
253     # The cachedgroupmembers table is used for unrolling group memberships
254     # to allow fast lookups. if we bind to CachedGroupMembers, we'll find
255     # all members of groups recursively. if we don't we'll find only 'direct'
256     # members of the group in question
257     my $group_members;
258     if ( $args{'IncludeSubgroupMembers'} ) {
259         $group_members = $self->NewAlias('CachedGroupMembers');
260     }
261     else {
262         $group_members = $self->NewAlias('GroupMembers');
263     }
264
265     $self->Join(
266         ALIAS1 => $group_members,
267         FIELD1 => 'MemberId',
268         ALIAS2 => $principals,
269         FIELD2 => 'id'
270     );
271     $self->Limit(
272         ALIAS => $group_members,
273         FIELD => 'Disabled',
274         VALUE => 0,
275     ) if $args{'IncludeSubgroupMembers'};
276
277     return $group_members;
278 }
279
280 # XXX: should be generalized
281 sub _JoinGroups
282 {
283     my $self = shift;
284     my %args = (@_);
285
286     my $group_members = $self->_JoinGroupMembers( %args );
287     my $groups = $self->NewAlias('Groups');
288     $self->Join(
289         ALIAS1 => $groups,
290         FIELD1 => 'id',
291         ALIAS2 => $group_members,
292         FIELD2 => 'GroupId'
293     );
294
295     return $groups;
296 }
297
298 # XXX: should be generalized
299 sub _JoinACL
300 {
301     my $self = shift;
302     my %args = (
303         Right                  => undef,
304         IncludeSuperusers      => undef,
305         @_,
306     );
307
308     if ( $args{'Right'} ) {
309         my $canonic = RT::ACE->CanonicalizeRightName( $args{'Right'} );
310         unless ( $canonic ) {
311             $RT::Logger->error("Invalid right. Couldn't canonicalize right '$args{'Right'}'");
312         }
313         else {
314             $args{'Right'} = $canonic;
315         }
316     }
317
318     my $acl = $self->NewAlias('ACL');
319     $self->Limit(
320         ALIAS    => $acl,
321         FIELD    => 'RightName',
322         OPERATOR => ( $args{Right} ? '=' : 'IS NOT' ),
323         VALUE => $args{Right} || 'NULL',
324         ENTRYAGGREGATOR => 'OR'
325     );
326     if ( $args{'IncludeSuperusers'} and $args{'Right'} ) {
327         $self->Limit(
328             ALIAS           => $acl,
329             FIELD           => 'RightName',
330             OPERATOR        => '=',
331             VALUE           => 'SuperUser',
332             ENTRYAGGREGATOR => 'OR'
333         );
334     }
335     return $acl;
336 }
337
338 # XXX: should be generalized
339 sub _GetEquivObjects
340 {
341     my $self = shift;
342     my %args = (
343         Object                 => undef,
344         IncludeSystemRights    => undef,
345         EquivObjects           => [ ],
346         @_
347     );
348     return () unless $args{'Object'};
349
350     my @objects = ($args{'Object'});
351     if ( UNIVERSAL::isa( $args{'Object'}, 'RT::Ticket' ) ) {
352         # If we're looking at ticket rights, we also want to look at the associated queue rights.
353         # this is a little bit hacky, but basically, now that we've done the ticket roles magic,
354         # we load the queue object and ask all the rest of our questions about the queue.
355
356         # XXX: This should be abstracted into object itself
357         if( $args{'Object'}->id ) {
358             push @objects, $args{'Object'}->ACLEquivalenceObjects;
359         } else {
360             push @objects, 'RT::Queue';
361         }
362     }
363
364     if( $args{'IncludeSystemRights'} ) {
365         push @objects, $RT::System;
366     }
367     push @objects, @{ $args{'EquivObjects'} };
368     return grep $_, @objects;
369 }
370
371 # XXX: should be generalized
372 sub WhoHaveRight {
373     my $self = shift;
374     my %args = (
375         Right                  => undef,
376         Object                 => undef,
377         IncludeSystemRights    => undef,
378         IncludeSuperusers      => undef,
379         IncludeSubgroupMembers => 1,
380         EquivObjects           => [ ],
381         @_
382     );
383
384     if ( defined $args{'ObjectType'} || defined $args{'ObjectId'} ) {
385         $RT::Logger->crit( "WhoHaveRight called with the Obsolete ObjectId/ObjectType API");
386         return (undef);
387     }
388
389     my $from_role = $self->Clone;
390     $from_role->WhoHaveRoleRight( %args );
391
392     my $from_group = $self->Clone;
393     $from_group->WhoHaveGroupRight( %args );
394
395     #XXX: DIRTY HACK
396     use DBIx::SearchBuilder::Union;
397     my $union = DBIx::SearchBuilder::Union->new();
398     $union->add( $from_group );
399     $union->add( $from_role );
400     %$self = %$union;
401     bless $self, ref($union);
402
403     return;
404 }
405
406 # XXX: should be generalized
407 sub WhoHaveRoleRight
408 {
409     my $self = shift;
410     my %args = (
411         Right                  => undef,
412         Object                 => undef,
413         IncludeSystemRights    => undef,
414         IncludeSuperusers      => undef,
415         IncludeSubgroupMembers => 1,
416         EquivObjects           => [ ],
417         @_
418     );
419
420     my @objects = $self->_GetEquivObjects( %args );
421
422     # RT::Principal->RolesWithRight only expects EquivObjects, so we need to
423     # fill it.  At the very least it needs $args{Object}, which
424     # _GetEquivObjects above does for us.
425     unshift @{$args{'EquivObjects'}}, @objects;
426
427     my @roles = RT::Principal->RolesWithRight( %args );
428     unless ( @roles ) {
429         $self->_AddSubClause( "WhichRole", "(main.id = 0)" );
430         return;
431     }
432
433     my $groups = $self->_JoinGroups( %args );
434
435     # no system user
436     $self->Limit( ALIAS => $self->PrincipalsAlias,
437                   FIELD => 'id',
438                   OPERATOR => '!=',
439                   VALUE => RT->SystemUser->id
440                 );
441
442     $self->_AddSubClause( "WhichRole", "(". join( ' OR ',
443         map $RT::Handle->__MakeClauseCaseInsensitive("$groups.Name", '=', "'$_'"), @roles
444     ) .")" );
445
446     my @groups_clauses = $self->_RoleClauses( $groups, @objects );
447     $self->_AddSubClause( "WhichObject", "(". join( ' OR ', @groups_clauses ) .")" )
448         if @groups_clauses;
449
450     return;
451 }
452
453 sub _RoleClauses {
454     my $self = shift;
455     my $groups = shift;
456     my @objects = @_;
457
458     my @groups_clauses;
459     foreach my $obj ( @objects ) {
460         my $type = ref($obj)? ref($obj): $obj;
461
462         my $role_clause = $RT::Handle->__MakeClauseCaseInsensitive("$groups.Domain", '=', "'$type-Role'");
463
464         if ( my $id = eval { $obj->id } ) {
465             $role_clause .= " AND $groups.Instance = $id";
466         }
467         push @groups_clauses, "($role_clause)";
468     }
469     return @groups_clauses;
470 }
471
472 # XXX: should be generalized
473 sub _JoinGroupMembersForGroupRights
474 {
475     my $self = shift;
476     my %args = (@_);
477     my $group_members = $self->_JoinGroupMembers( %args );
478     $self->Limit( ALIAS => $args{'ACLAlias'},
479                   FIELD => 'PrincipalId',
480                   VALUE => "$group_members.GroupId",
481                   QUOTEVALUE => 0,
482                 );
483     return $group_members;
484 }
485
486 # XXX: should be generalized
487 sub WhoHaveGroupRight
488 {
489     my $self = shift;
490     my %args = (
491         Right                  => undef,
492         Object                 => undef,
493         IncludeSystemRights    => undef,
494         IncludeSuperusers      => undef,
495         IncludeSubgroupMembers => 1,
496         EquivObjects           => [ ],
497         @_
498     );
499
500     # Find only rows where the right granted is
501     # the one we're looking up or _possibly_ superuser
502     my $acl = $self->_JoinACL( %args );
503
504     my ($check_objects) = ('');
505     my @objects = $self->_GetEquivObjects( %args );
506
507     my %seen;
508     if ( @objects ) {
509         my @object_clauses;
510         foreach my $obj ( @objects ) {
511             my $type = ref($obj)? ref($obj): $obj;
512             my $id = 0;
513             $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
514             next if $seen{"$type-$id"}++;
515
516             my $object_clause = "$acl.ObjectType = '$type'";
517             $object_clause   .= " AND $acl.ObjectId   = $id" if $id;
518             push @object_clauses, "($object_clause)";
519         }
520
521         $check_objects = join ' OR ', @object_clauses;
522     } else {
523         if( !$args{'IncludeSystemRights'} ) {
524             $check_objects = "($acl.ObjectType != 'RT::System')";
525         }
526     }
527     $self->_AddSubClause( "WhichObject", "($check_objects)" );
528     
529     my $group_members = $self->_JoinGroupMembersForGroupRights( %args, ACLAlias => $acl );
530     # Find only members of groups that have the right.
531     $self->Limit( ALIAS => $acl,
532                   FIELD => 'PrincipalType',
533                   VALUE => 'Group',
534                 );
535     
536     # no system user
537     $self->Limit( ALIAS => $self->PrincipalsAlias,
538                   FIELD => 'id',
539                   OPERATOR => '!=',
540                   VALUE => RT->SystemUser->id
541                 );
542     return $group_members;
543 }
544
545
546 =head2 WhoBelongToGroups { Groups => ARRAYREF, IncludeSubgroupMembers => 1, IncludeUnprivileged => 0 }
547
548 Return members who belong to any of the groups passed in the groups whose IDs
549 are included in the Groups arrayref.
550
551 If IncludeSubgroupMembers is true (default) then members of any group that's a
552 member of one of the passed groups are returned. If it's cleared then only
553 direct member users are returned.
554
555 If IncludeUnprivileged is false (default) then only privileged members are
556 returned; otherwise either privileged or unprivileged group members may be
557 returned.
558
559 =cut
560
561 sub WhoBelongToGroups {
562     my $self = shift;
563     my %args = ( Groups                 => undef,
564                  IncludeSubgroupMembers => 1,
565                  IncludeUnprivileged    => 0,
566                  @_ );
567
568     if (!$args{'IncludeUnprivileged'}) {
569         $self->LimitToPrivileged();
570     }
571     my $group_members = $self->_JoinGroupMembers( %args );
572
573     foreach my $groupid (@{$args{'Groups'}}) {
574         $self->Limit( ALIAS           => $group_members,
575                       FIELD           => 'GroupId',
576                       VALUE           => $groupid,
577                       QUOTEVALUE      => 0,
578                       ENTRYAGGREGATOR => 'OR',
579                     );
580     }
581 }
582
583 =head2 SimpleSearch
584
585 Does a 'simple' search of Users against a specified Term.
586
587 This Term is compared to a number of fields using various types of SQL
588 comparison operators.
589
590 Ensures that the returned collection of Users will have a value for Return.
591
592 This method is passed the following.  You must specify a Term and a Return.
593
594     Privileged - Whether or not to limit to Privileged Users (0 or 1)
595     Fields     - Hashref of data - defaults to C<$UserSearchFields> emulate that if you want to override
596     Term       - String that is in the fields specified by Fields
597     Return     - What field on the User you want to be sure isn't empty
598     Exclude    - Array reference of ids to exclude
599     Max        - What to limit this collection to
600
601 =cut
602
603 sub SimpleSearch {
604     my $self = shift;
605     my %args = (
606         Privileged  => 0,
607         Fields      => RT->Config->Get('UserSearchFields'),
608         Term        => undef,
609         Exclude     => [],
610         Return      => undef,
611         Max         => 10,
612         @_
613     );
614
615     return $self unless defined $args{Return}
616                         and defined $args{Term}
617                         and length $args{Term};
618
619     $self->RowsPerPage( $args{Max} );
620
621     $self->LimitToPrivileged() if $args{Privileged};
622
623     while (my ($name, $op) = each %{$args{Fields}}) {
624         $op = 'STARTSWITH'
625         unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
626
627         if ($name =~ /^CF\.(?:\{(.*)}|(.*))$/) {
628             my $cfname = $1 || $2;
629             $self->LimitCustomField(
630                 CUSTOMFIELD     => $cfname,
631                 OPERATOR        => $op,
632                 VALUE           => $args{Term},
633                 ENTRYAGGREGATOR => 'OR',
634                 SUBCLAUSE       => 'autocomplete',
635             );
636         } else {
637             $self->Limit(
638                 FIELD           => $name,
639                 OPERATOR        => $op,
640                 VALUE           => $args{Term},
641                 ENTRYAGGREGATOR => 'OR',
642                 SUBCLAUSE       => 'autocomplete',
643             );
644         }
645     }
646
647     # Exclude users we don't want
648     foreach (@{$args{Exclude}}) {
649         $self->Limit(FIELD => 'id', VALUE => $_, OPERATOR => '!=', ENTRYAGGREGATOR => 'AND');
650     }
651
652     if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
653         $self->Limit(
654             FIELD    => $args{Return},
655             OPERATOR => 'IS NOT',
656             VALUE    => 'NULL',
657         );
658     }
659     else {
660         $self->Limit( FIELD => $args{Return}, OPERATOR => '!=', VALUE => '' );
661         $self->Limit(
662             FIELD           => $args{Return},
663             OPERATOR        => 'IS NOT',
664             VALUE           => 'NULL',
665             ENTRYAGGREGATOR => 'AND'
666         );
667     }
668
669     return $self;
670 }
671
672 RT::Base->_ImportOverlays();
673
674 1;