Putting 4.2.0 on top of 4.0.17
[usit-rt.git] / lib / RT / Users.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 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     if ( @objects ) {
508         my @object_clauses;
509         foreach my $obj ( @objects ) {
510             my $type = ref($obj)? ref($obj): $obj;
511             my $id;
512             $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
513
514             my $object_clause = "$acl.ObjectType = '$type'";
515             $object_clause   .= " AND $acl.ObjectId   = $id" if $id;
516             push @object_clauses, "($object_clause)";
517         }
518
519         $check_objects = join ' OR ', @object_clauses;
520     } else {
521         if( !$args{'IncludeSystemRights'} ) {
522             $check_objects = "($acl.ObjectType != 'RT::System')";
523         }
524     }
525     $self->_AddSubClause( "WhichObject", "($check_objects)" );
526     
527     my $group_members = $self->_JoinGroupMembersForGroupRights( %args, ACLAlias => $acl );
528     # Find only members of groups that have the right.
529     $self->Limit( ALIAS => $acl,
530                   FIELD => 'PrincipalType',
531                   VALUE => 'Group',
532                 );
533     
534     # no system user
535     $self->Limit( ALIAS => $self->PrincipalsAlias,
536                   FIELD => 'id',
537                   OPERATOR => '!=',
538                   VALUE => RT->SystemUser->id
539                 );
540     return $group_members;
541 }
542
543
544 =head2 WhoBelongToGroups { Groups => ARRAYREF, IncludeSubgroupMembers => 1 }
545
546 =cut
547
548 # XXX: should be generalized
549 sub WhoBelongToGroups {
550     my $self = shift;
551     my %args = ( Groups                 => undef,
552                  IncludeSubgroupMembers => 1,
553                  @_ );
554
555     # Unprivileged users can't be granted real system rights.
556     # is this really the right thing to be saying?
557     $self->LimitToPrivileged();
558
559     my $group_members = $self->_JoinGroupMembers( %args );
560
561     foreach my $groupid (@{$args{'Groups'}}) {
562         $self->Limit( ALIAS           => $group_members,
563                       FIELD           => 'GroupId',
564                       VALUE           => $groupid,
565                       QUOTEVALUE      => 0,
566                       ENTRYAGGREGATOR => 'OR',
567                     );
568     }
569 }
570
571 =head2 SimpleSearch
572
573 Does a 'simple' search of Users against a specified Term.
574
575 This Term is compared to a number of fields using various types of SQL
576 comparison operators.
577
578 Ensures that the returned collection of Users will have a value for Return.
579
580 This method is passed the following.  You must specify a Term and a Return.
581
582     Privileged - Whether or not to limit to Privileged Users (0 or 1)
583     Fields     - Hashref of data - defaults to C<$UserSearchFields> emulate that if you want to override
584     Term       - String that is in the fields specified by Fields
585     Return     - What field on the User you want to be sure isn't empty
586     Exclude    - Array reference of ids to exclude
587     Max        - What to limit this collection to
588
589 =cut
590
591 sub SimpleSearch {
592     my $self = shift;
593     my %args = (
594         Privileged  => 0,
595         Fields      => RT->Config->Get('UserSearchFields'),
596         Term        => undef,
597         Exclude     => [],
598         Return      => undef,
599         Max         => 10,
600         @_
601     );
602
603     return $self unless defined $args{Return}
604                         and defined $args{Term}
605                         and length $args{Term};
606
607     $self->RowsPerPage( $args{Max} );
608
609     $self->LimitToPrivileged() if $args{Privileged};
610
611     while (my ($name, $op) = each %{$args{Fields}}) {
612         $op = 'STARTSWITH'
613         unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
614
615         if ($name =~ /^CF\.(?:\{(.*)}|(.*))$/) {
616             my $cfname = $1 || $2;
617             $self->LimitCustomField(
618                 CUSTOMFIELD     => $cfname,
619                 OPERATOR        => $op,
620                 VALUE           => $args{Term},
621                 ENTRYAGGREGATOR => 'OR',
622                 SUBCLAUSE       => 'autocomplete',
623             );
624         } else {
625             $self->Limit(
626                 FIELD           => $name,
627                 OPERATOR        => $op,
628                 VALUE           => $args{Term},
629                 ENTRYAGGREGATOR => 'OR',
630                 SUBCLAUSE       => 'autocomplete',
631             );
632         }
633     }
634
635     # Exclude users we don't want
636     foreach (@{$args{Exclude}}) {
637         $self->Limit(FIELD => 'id', VALUE => $_, OPERATOR => '!=', ENTRYAGGREGATOR => 'AND');
638     }
639
640     if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
641         $self->Limit(
642             FIELD    => $args{Return},
643             OPERATOR => 'IS NOT',
644             VALUE    => 'NULL',
645         );
646     }
647     else {
648         $self->Limit( FIELD => $args{Return}, OPERATOR => '!=', VALUE => '' );
649         $self->Limit(
650             FIELD           => $args{Return},
651             OPERATOR        => 'IS NOT',
652             VALUE           => 'NULL',
653             ENTRYAGGREGATOR => 'AND'
654         );
655     }
656
657     return $self;
658 }
659
660 RT::Base->_ImportOverlays();
661
662 1;