Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / Users.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2015 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 1.50; #no version on ::Union :(
397     use DBIx::SearchBuilder::Union;
398     my $union = DBIx::SearchBuilder::Union->new();
399     $union->add( $from_group );
400     $union->add( $from_role );
401     %$self = %$union;
402     bless $self, ref($union);
403
404     return;
405 }
406
407 # XXX: should be generalized
408 sub WhoHaveRoleRight
409 {
410     my $self = shift;
411     my %args = (
412         Right                  => undef,
413         Object                 => undef,
414         IncludeSystemRights    => undef,
415         IncludeSuperusers      => undef,
416         IncludeSubgroupMembers => 1,
417         EquivObjects           => [ ],
418         @_
419     );
420
421     my @objects = $self->_GetEquivObjects( %args );
422
423     # RT::Principal->RolesWithRight only expects EquivObjects, so we need to
424     # fill it.  At the very least it needs $args{Object}, which
425     # _GetEquivObjects above does for us.
426     unshift @{$args{'EquivObjects'}}, @objects;
427
428     my @roles = RT::Principal->RolesWithRight( %args );
429     unless ( @roles ) {
430         $self->_AddSubClause( "WhichRole", "(main.id = 0)" );
431         return;
432     }
433
434     my $groups = $self->_JoinGroups( %args );
435
436     # no system user
437     $self->Limit( ALIAS => $self->PrincipalsAlias,
438                   FIELD => 'id',
439                   OPERATOR => '!=',
440                   VALUE => RT->SystemUser->id
441                 );
442
443     $self->_AddSubClause( "WhichRole", "(". join( ' OR ',
444         map $RT::Handle->__MakeClauseCaseInsensitive("$groups.Name", '=', "'$_'"), @roles
445     ) .")" );
446
447     my @groups_clauses = $self->_RoleClauses( $groups, @objects );
448     $self->_AddSubClause( "WhichObject", "(". join( ' OR ', @groups_clauses ) .")" )
449         if @groups_clauses;
450
451     return;
452 }
453
454 sub _RoleClauses {
455     my $self = shift;
456     my $groups = shift;
457     my @objects = @_;
458
459     my @groups_clauses;
460     foreach my $obj ( @objects ) {
461         my $type = ref($obj)? ref($obj): $obj;
462
463         my $role_clause = $RT::Handle->__MakeClauseCaseInsensitive("$groups.Domain", '=', "'$type-Role'");
464
465         if ( my $id = eval { $obj->id } ) {
466             $role_clause .= " AND $groups.Instance = $id";
467         }
468         push @groups_clauses, "($role_clause)";
469     }
470     return @groups_clauses;
471 }
472
473 # XXX: should be generalized
474 sub _JoinGroupMembersForGroupRights
475 {
476     my $self = shift;
477     my %args = (@_);
478     my $group_members = $self->_JoinGroupMembers( %args );
479     $self->Limit( ALIAS => $args{'ACLAlias'},
480                   FIELD => 'PrincipalId',
481                   VALUE => "$group_members.GroupId",
482                   QUOTEVALUE => 0,
483                 );
484     return $group_members;
485 }
486
487 # XXX: should be generalized
488 sub WhoHaveGroupRight
489 {
490     my $self = shift;
491     my %args = (
492         Right                  => undef,
493         Object                 => undef,
494         IncludeSystemRights    => undef,
495         IncludeSuperusers      => undef,
496         IncludeSubgroupMembers => 1,
497         EquivObjects           => [ ],
498         @_
499     );
500
501     # Find only rows where the right granted is
502     # the one we're looking up or _possibly_ superuser
503     my $acl = $self->_JoinACL( %args );
504
505     my ($check_objects) = ('');
506     my @objects = $self->_GetEquivObjects( %args );
507
508     my %seen;
509     if ( @objects ) {
510         my @object_clauses;
511         foreach my $obj ( @objects ) {
512             my $type = ref($obj)? ref($obj): $obj;
513             my $id = 0;
514             $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
515             next if $seen{"$type-$id"}++;
516
517             my $object_clause = "$acl.ObjectType = '$type'";
518             $object_clause   .= " AND $acl.ObjectId   = $id" if $id;
519             push @object_clauses, "($object_clause)";
520         }
521
522         $check_objects = join ' OR ', @object_clauses;
523     } else {
524         if( !$args{'IncludeSystemRights'} ) {
525             $check_objects = "($acl.ObjectType != 'RT::System')";
526         }
527     }
528     $self->_AddSubClause( "WhichObject", "($check_objects)" );
529     
530     my $group_members = $self->_JoinGroupMembersForGroupRights( %args, ACLAlias => $acl );
531     # Find only members of groups that have the right.
532     $self->Limit( ALIAS => $acl,
533                   FIELD => 'PrincipalType',
534                   VALUE => 'Group',
535                 );
536     
537     # no system user
538     $self->Limit( ALIAS => $self->PrincipalsAlias,
539                   FIELD => 'id',
540                   OPERATOR => '!=',
541                   VALUE => RT->SystemUser->id
542                 );
543     return $group_members;
544 }
545
546
547 =head2 WhoBelongToGroups { Groups => ARRAYREF, IncludeSubgroupMembers => 1, IncludeUnprivileged => 0 }
548
549 Return members who belong to any of the groups passed in the groups whose IDs
550 are included in the Groups arrayref.
551
552 If IncludeSubgroupMembers is true (default) then members of any group that's a
553 member of one of the passed groups are returned. If it's cleared then only
554 direct member users are returned.
555
556 If IncludeUnprivileged is false (default) then only privileged members are
557 returned; otherwise either privileged or unprivileged group members may be
558 returned.
559
560 =cut
561
562 sub WhoBelongToGroups {
563     my $self = shift;
564     my %args = ( Groups                 => undef,
565                  IncludeSubgroupMembers => 1,
566                  IncludeUnprivileged    => 0,
567                  @_ );
568
569     if (!$args{'IncludeUnprivileged'}) {
570         $self->LimitToPrivileged();
571     }
572     my $group_members = $self->_JoinGroupMembers( %args );
573
574     $self->Limit(
575         ALIAS      => $group_members,
576         FIELD      => 'GroupId',
577         OPERATOR   => 'IN',
578         VALUE      => [ 0, @{$args{'Groups'}} ],
579     );
580 }
581
582 =head2 SimpleSearch
583
584 Does a 'simple' search of Users against a specified Term.
585
586 This Term is compared to a number of fields using various types of SQL
587 comparison operators.
588
589 Ensures that the returned collection of Users will have a value for Return.
590
591 This method is passed the following.  You must specify a Term and a Return.
592
593     Privileged - Whether or not to limit to Privileged Users (0 or 1)
594     Fields     - Hashref of data - defaults to C<$UserSearchFields> emulate that if you want to override
595     Term       - String that is in the fields specified by Fields
596     Return     - What field on the User you want to be sure isn't empty
597     Exclude    - Array reference of ids to exclude
598     Max        - What to limit this collection to
599
600 =cut
601
602 sub SimpleSearch {
603     my $self = shift;
604     my %args = (
605         Privileged  => 0,
606         Fields      => RT->Config->Get('UserSearchFields'),
607         Term        => undef,
608         Exclude     => [],
609         Return      => undef,
610         Max         => 10,
611         @_
612     );
613
614     return $self unless defined $args{Return}
615                         and defined $args{Term}
616                         and length $args{Term};
617
618     $self->RowsPerPage( $args{Max} );
619
620     $self->LimitToPrivileged() if $args{Privileged};
621
622     while (my ($name, $op) = each %{$args{Fields}}) {
623         $op = 'STARTSWITH'
624         unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
625
626         if ($name =~ /^CF\.(?:\{(.*)}|(.*))$/) {
627             my $cfname = $1 || $2;
628             my $cf = RT::CustomField->new(RT->SystemUser);
629             my ($ok, $msg) = $cf->LoadByName( Name => $cfname, LookupType => 'RT::User');
630             if ( $ok ) {
631                 $self->LimitCustomField(
632                     CUSTOMFIELD     => $cf->Id,
633                     OPERATOR        => $op,
634                     VALUE           => $args{Term},
635                     ENTRYAGGREGATOR => 'OR',
636                     SUBCLAUSE       => 'autocomplete',
637                 );
638             } else {
639                 RT->Logger->warning("Asked to search custom field $name but unable to load a User CF with the name $cfname: $msg");
640             }
641         } else {
642             $self->Limit(
643                 FIELD           => $name,
644                 OPERATOR        => $op,
645                 VALUE           => $args{Term},
646                 ENTRYAGGREGATOR => 'OR',
647                 SUBCLAUSE       => 'autocomplete',
648             );
649         }
650     }
651
652     # Exclude users we don't want
653     $self->Limit(FIELD => 'id', OPERATOR => 'NOT IN', VALUE => $args{Exclude} )
654         if @{$args{Exclude}};
655
656     if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
657         $self->Limit(
658             FIELD    => $args{Return},
659             OPERATOR => 'IS NOT',
660             VALUE    => 'NULL',
661         );
662     }
663     else {
664         $self->Limit( FIELD => $args{Return}, OPERATOR => '!=', VALUE => '' );
665         $self->Limit(
666             FIELD           => $args{Return},
667             OPERATOR        => 'IS NOT',
668             VALUE           => 'NULL',
669             ENTRYAGGREGATOR => 'AND'
670         );
671     }
672
673     return $self;
674 }
675
676 RT::Base->_ImportOverlays();
677
678 1;