1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 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
67package RT::Users;
68
69use strict;
70use warnings;
71
72use base 'RT::SearchBuilder';
73
74use RT::User;
75
76sub Table { 'Users'}
77
78
79sub _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
103sub OrderByCols {
104    my $self = shift;
105    my @res  = ();
106
107    for my $row (@_) {
108        if (($row->{FIELD}||'') =~ /^CustomField\.\{(.*)\}$/) {
109            my $name = $1 || $2;
110            my $cf = RT::CustomField->new( $self->CurrentUser );
111            $cf->LoadByName(
112                Name => $name,
113                ObjectType => 'RT::User',
114            );
115            if ( $cf->id ) {
116                push @res, $self->_OrderByCF( $row, $cf->id, $cf );
117            }
118        } else {
119            push @res, $row;
120        }
121    }
122    return $self->SUPER::OrderByCols( @res );
123}
124
125=head2 PrincipalsAlias
126
127Returns the string that represents this Users object's primary "Principals" alias.
128
129=cut
130
131# XXX: should be generalized
132sub PrincipalsAlias {
133    my $self = shift;
134    return($self->{'princalias'});
135
136}
137
138
139=head2 LimitToEnabled
140
141Only find items that haven't been disabled
142
143=cut
144
145# XXX: should be generalized
146sub LimitToEnabled {
147    my $self = shift;
148
149    $self->{'handled_disabled_column'} = 1;
150    $self->Limit(
151        ALIAS    => $self->PrincipalsAlias,
152        FIELD    => 'Disabled',
153        VALUE    => '0',
154    );
155}
156
157=head2 LimitToDeleted
158
159Only find items that have been deleted.
160
161=cut
162
163sub LimitToDeleted {
164    my $self = shift;
165
166    $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1;
167    $self->Limit(
168        ALIAS => $self->PrincipalsAlias,
169        FIELD => 'Disabled',
170        VALUE => 1,
171    );
172}
173
174
175
176=head2 LimitToEmail
177
178Takes one argument. an email address. limits the returned set to
179that email address
180
181=cut
182
183sub LimitToEmail {
184    my $self = shift;
185    my $addr = shift;
186    $self->Limit( FIELD => 'EmailAddress', VALUE => $addr, CASESENSITIVE => 0 );
187}
188
189
190
191=head2 MemberOfGroup PRINCIPAL_ID
192
193takes one argument, a group's principal id. Limits the returned set
194to members of a given group
195
196=cut
197
198sub MemberOfGroup {
199    my $self  = shift;
200    my $group = shift;
201
202    return $self->loc("No group specified") if ( !defined $group );
203
204    my $groupalias = $self->NewAlias('CachedGroupMembers');
205
206    # Join the principal to the groups table
207    $self->Join( ALIAS1 => $self->PrincipalsAlias,
208                 FIELD1 => 'id',
209                 ALIAS2 => $groupalias,
210                 FIELD2 => 'MemberId' );
211    $self->Limit( ALIAS => $groupalias,
212                  FIELD => 'Disabled',
213                  VALUE => 0 );
214
215    $self->Limit( ALIAS    => "$groupalias",
216                  FIELD    => 'GroupId',
217                  VALUE    => "$group",
218                  OPERATOR => "=" );
219}
220
221
222
223=head2 LimitToPrivileged
224
225Limits to users who can be made members of ACLs and groups
226
227=cut
228
229sub LimitToPrivileged {
230    my $self = shift;
231    $self->MemberOfGroup( RT->PrivilegedUsers->id );
232}
233
234=head2 LimitToUnprivileged
235
236Limits to unprivileged users only
237
238=cut
239
240sub LimitToUnprivileged {
241    my $self = shift;
242    $self->MemberOfGroup( RT->UnprivilegedUsers->id);
243}
244
245=head2 LimitToEndUsers
246
247Limits to end users only, i.e. no internal users "RT_System" and "Nobody".
248
249=cut
250
251sub LimitToEndUsers {
252    my $self = shift;
253    for my $user ( RT->SystemUser, RT->Nobody ) {
254        $self->Limit( FIELD => 'id', VALUE => $user->Id, OPERATOR => '!=', ENTRYAGGREGATOR => 'AND' );
255    }
256}
257
258sub Limit {
259    my $self = shift;
260    my %args = @_;
261    $args{'CASESENSITIVE'} = 0 unless exists $args{'CASESENSITIVE'} or $args{'ALIAS'};
262    return $self->SUPER::Limit( %args );
263}
264
265=head2 WhoHaveRight { Right => 'name', Object => $rt_object , IncludeSuperusers => undef, IncludeSubgroupMembers => undef, IncludeSystemRights => undef, EquivObjects => [ ] }
266
267
268find all users who the right Right for this group, either individually
269or as members of groups
270
271If passed a queue object, with no id, it will find users who have that right for _any_ queue
272
273=cut
274
275# XXX: should be generalized
276sub _JoinGroupMembers
277{
278    my $self = shift;
279    my %args = (
280        IncludeSubgroupMembers => 1,
281        @_
282    );
283
284    my $principals = $self->PrincipalsAlias;
285
286    # The cachedgroupmembers table is used for unrolling group memberships
287    # to allow fast lookups. if we bind to CachedGroupMembers, we'll find
288    # all members of groups recursively. if we don't we'll find only 'direct'
289    # members of the group in question
290    my $group_members;
291    if ( $args{'IncludeSubgroupMembers'} ) {
292        $group_members = $self->NewAlias('CachedGroupMembers');
293    }
294    else {
295        $group_members = $self->NewAlias('GroupMembers');
296    }
297
298    $self->Join(
299        ALIAS1 => $group_members,
300        FIELD1 => 'MemberId',
301        ALIAS2 => $principals,
302        FIELD2 => 'id'
303    );
304    $self->Limit(
305        ALIAS => $group_members,
306        FIELD => 'Disabled',
307        VALUE => 0,
308    ) if $args{'IncludeSubgroupMembers'};
309
310    return $group_members;
311}
312
313# XXX: should be generalized
314sub _JoinGroups
315{
316    my $self = shift;
317    my %args = (@_);
318
319    my $group_members = $self->_JoinGroupMembers( %args );
320    my $groups = $self->NewAlias('Groups');
321    $self->Join(
322        ALIAS1 => $groups,
323        FIELD1 => 'id',
324        ALIAS2 => $group_members,
325        FIELD2 => 'GroupId'
326    );
327
328    return $groups;
329}
330
331# XXX: should be generalized
332sub _JoinACL
333{
334    my $self = shift;
335    my %args = (
336        Right                  => undef,
337        IncludeSuperusers      => undef,
338        @_,
339    );
340
341    if ( $args{'Right'} ) {
342        my $canonic = RT::ACE->CanonicalizeRightName( $args{'Right'} );
343        unless ( $canonic ) {
344            $RT::Logger->error("Invalid right. Couldn't canonicalize right '$args{'Right'}'");
345        }
346        else {
347            $args{'Right'} = $canonic;
348        }
349    }
350
351    my $acl = $self->NewAlias('ACL');
352    if ( $args{Right} ) {
353        if ( $args{'IncludeSuperusers'} && $args{Right} ne 'SuperUser' ) {
354            $self->Limit(
355                ALIAS    => $acl,
356                FIELD    => 'RightName',
357                OPERATOR => 'IN',
358                VALUE    => [ 'SuperUser', $args{Right} ],
359            );
360        }
361        else {
362            $self->Limit(
363                ALIAS    => $acl,
364                FIELD    => 'RightName',
365                OPERATOR => '=',
366                VALUE    => $args{Right},
367            );
368        }
369    }
370    else {
371        $self->Limit(
372            ALIAS    => $acl,
373            FIELD    => 'RightName',
374            OPERATOR => 'IS NOT',
375            VALUE    => 'NULL',
376        );
377    }
378    return $acl;
379}
380
381# XXX: should be generalized
382sub _GetEquivObjects
383{
384    my $self = shift;
385    my %args = (
386        Object                 => undef,
387        IncludeSystemRights    => undef,
388        EquivObjects           => [ ],
389        @_
390    );
391    return () unless $args{'Object'};
392
393    my @objects = ($args{'Object'});
394    if ( UNIVERSAL::isa( $args{'Object'}, 'RT::Ticket' ) ) {
395        # If we're looking at ticket rights, we also want to look at the associated queue rights.
396        # this is a little bit hacky, but basically, now that we've done the ticket roles magic,
397        # we load the queue object and ask all the rest of our questions about the queue.
398
399        # XXX: This should be abstracted into object itself
400        if( $args{'Object'}->id ) {
401            push @objects, $args{'Object'}->ACLEquivalenceObjects;
402        } else {
403            push @objects, 'RT::Queue';
404        }
405    }
406
407    if( $args{'IncludeSystemRights'} ) {
408        push @objects, $RT::System;
409    }
410    push @objects, @{ $args{'EquivObjects'} };
411    return grep $_, @objects;
412}
413
414# XXX: should be generalized
415sub WhoHaveRight {
416    my $self = shift;
417    my %args = (
418        Right                  => undef,
419        Object                 => undef,
420        IncludeSystemRights    => undef,
421        IncludeSuperusers      => undef,
422        IncludeSubgroupMembers => 1,
423        EquivObjects           => [ ],
424        @_
425    );
426
427    if ( defined $args{'ObjectType'} || defined $args{'ObjectId'} ) {
428        $RT::Logger->crit( "WhoHaveRight called with the Obsolete ObjectId/ObjectType API");
429        return (undef);
430    }
431
432    my $from_role = $self->Clone;
433    $from_role->WhoHaveRoleRight( %args );
434
435    my $from_group = $self->Clone;
436    $from_group->WhoHaveGroupRight( %args );
437
438    #XXX: DIRTY HACK
439    use DBIx::SearchBuilder::Union;
440    my $union = DBIx::SearchBuilder::Union->new();
441    $union->add( $from_group );
442    $union->add( $from_role );
443    %$self = %$union;
444    bless $self, ref($union);
445
446    return;
447}
448
449# XXX: should be generalized
450sub WhoHaveRoleRight
451{
452    my $self = shift;
453    my %args = (
454        Right                  => undef,
455        Object                 => undef,
456        IncludeSystemRights    => undef,
457        IncludeSuperusers      => undef,
458        IncludeSubgroupMembers => 1,
459        EquivObjects           => [ ],
460        @_
461    );
462
463    my @objects = $self->_GetEquivObjects( %args );
464
465    # RT::Principal->RolesWithRight only expects EquivObjects, so we need to
466    # fill it.  At the very least it needs $args{Object}, which
467    # _GetEquivObjects above does for us.
468    unshift @{$args{'EquivObjects'}}, @objects;
469
470    my @roles = RT::Principal->RolesWithRight( %args );
471    unless ( @roles ) {
472        $self->_AddSubClause( "WhichRole", "(main.id = 0)" );
473        return;
474    }
475
476    my $groups = $self->_JoinGroups( %args );
477
478    # no system user
479    $self->Limit( ALIAS => $self->PrincipalsAlias,
480                  FIELD => 'id',
481                  OPERATOR => '!=',
482                  VALUE => RT->SystemUser->id
483                );
484
485    $self->_AddSubClause( "WhichRole", "(". join( ' OR ',
486        map $RT::Handle->__MakeClauseCaseInsensitive("$groups.Name", '=', "'$_'"), @roles
487    ) .")" );
488
489    my @groups_clauses = $self->_RoleClauses( $groups, @objects );
490    $self->_AddSubClause( "WhichObject", "(". join( ' OR ', @groups_clauses ) .")" )
491        if @groups_clauses;
492
493    return;
494}
495
496sub _RoleClauses {
497    my $self = shift;
498    my $groups = shift;
499    my @objects = @_;
500
501    my @groups_clauses;
502    foreach my $obj ( @objects ) {
503        my $type = ref($obj)? ref($obj): $obj;
504
505        my $role_clause = $RT::Handle->__MakeClauseCaseInsensitive("$groups.Domain", '=', "'$type-Role'");
506
507        if ( my $id = eval { $obj->id } ) {
508            $role_clause .= " AND $groups.Instance = $id";
509        }
510        push @groups_clauses, "($role_clause)";
511    }
512    return @groups_clauses;
513}
514
515# XXX: should be generalized
516sub _JoinGroupMembersForGroupRights
517{
518    my $self = shift;
519    my %args = (@_);
520    my $group_members = $self->_JoinGroupMembers( %args );
521    $self->Limit( ALIAS => $args{'ACLAlias'},
522                  FIELD => 'PrincipalId',
523                  VALUE => "$group_members.GroupId",
524                  QUOTEVALUE => 0,
525                );
526    return $group_members;
527}
528
529# XXX: should be generalized
530sub WhoHaveGroupRight
531{
532    my $self = shift;
533    my %args = (
534        Right                  => undef,
535        Object                 => undef,
536        IncludeSystemRights    => undef,
537        IncludeSuperusers      => undef,
538        IncludeSubgroupMembers => 1,
539        EquivObjects           => [ ],
540        @_
541    );
542
543    # Find only rows where the right granted is
544    # the one we're looking up or _possibly_ superuser
545    my $acl = $self->_JoinACL( %args );
546
547    my ($check_objects) = ('');
548    my @objects = $self->_GetEquivObjects( %args );
549
550    my %seen;
551    if ( @objects ) {
552        my @object_clauses;
553        foreach my $obj ( @objects ) {
554            my $type = ref($obj)? ref($obj): $obj;
555            my $id = 0;
556            $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
557            next if $seen{"$type-$id"}++;
558
559            my $object_clause = "$acl.ObjectType = '$type'";
560            $object_clause   .= " AND $acl.ObjectId   = $id" if $id;
561            push @object_clauses, "($object_clause)";
562        }
563
564        $check_objects = join ' OR ', @object_clauses;
565    } else {
566        if( !$args{'IncludeSystemRights'} ) {
567            $check_objects = "($acl.ObjectType != 'RT::System')";
568        }
569    }
570    $self->_AddSubClause( "WhichObject", "($check_objects)" );
571
572    my $group_members = $self->_JoinGroupMembersForGroupRights( %args, ACLAlias => $acl );
573    # Find only members of groups that have the right.
574    $self->Limit( ALIAS => $acl,
575                  FIELD => 'PrincipalType',
576                  VALUE => 'Group',
577                );
578
579    # no system user
580    $self->Limit( ALIAS => $self->PrincipalsAlias,
581                  FIELD => 'id',
582                  OPERATOR => '!=',
583                  VALUE => RT->SystemUser->id
584                );
585    return $group_members;
586}
587
588
589=head2 WhoBelongToGroups { Groups => ARRAYREF, IncludeSubgroupMembers => 1, IncludeUnprivileged => 0 }
590
591Return members who belong to any of the groups passed in the groups whose IDs
592are included in the Groups arrayref.
593
594If IncludeSubgroupMembers is true (default) then members of any group that's a
595member of one of the passed groups are returned. If it's cleared then only
596direct member users are returned.
597
598If IncludeUnprivileged is false (default) then only privileged members are
599returned; otherwise either privileged or unprivileged group members may be
600returned.
601
602=cut
603
604sub WhoBelongToGroups {
605    my $self = shift;
606    my %args = ( Groups                 => undef,
607                 IncludeSubgroupMembers => 1,
608                 IncludeUnprivileged    => 0,
609                 @_ );
610
611    if (!$args{'IncludeUnprivileged'}) {
612        $self->LimitToPrivileged();
613    }
614    my $group_members = $self->_JoinGroupMembers( %args );
615
616    $self->Limit(
617        ALIAS      => $group_members,
618        FIELD      => 'GroupId',
619        OPERATOR   => 'IN',
620        VALUE      => [ 0, @{$args{'Groups'}} ],
621    );
622}
623
624=head2 SimpleSearch
625
626Does a 'simple' search of Users against a specified Term.
627
628This Term is compared to a number of fields using various types of SQL
629comparison operators.
630
631Ensures that the returned collection of Users will have a value for Return.
632
633This method is passed the following.  You must specify a Term and a Return.
634
635    Privileged - Whether or not to limit to Privileged Users (0 or 1)
636    Fields     - Hashref of data - defaults to C<$UserSearchFields> emulate that if you want to override
637    Term       - String that is in the fields specified by Fields
638    Return     - What field on the User you want to be sure isn't empty
639    Exclude    - Array reference of ids to exclude
640    Max        - What to limit this collection to
641
642=cut
643
644sub SimpleSearch {
645    my $self = shift;
646    my %args = (
647        Privileged  => 0,
648        Fields      => RT->Config->Get('UserSearchFields'),
649        Term        => undef,
650        Exclude     => [],
651        Return      => undef,
652        Max         => 10,
653        @_
654    );
655
656    return $self unless defined $args{Return}
657                        and defined $args{Term}
658                        and length $args{Term};
659
660    $self->RowsPerPage( $args{Max} );
661
662    $self->LimitToPrivileged() if $args{Privileged};
663
664    while (my ($name, $op) = each %{$args{Fields}}) {
665        $op = 'STARTSWITH'
666        unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
667
668        if ($name =~ /^CF\.(?:\{(.*)}|(.*))$/) {
669            my $cfname = $1 || $2;
670            my $cf = RT::CustomField->new(RT->SystemUser);
671            my ($ok, $msg) = $cf->LoadByName( Name => $cfname, LookupType => 'RT::User');
672            if ( $ok ) {
673                $self->LimitCustomField(
674                    CUSTOMFIELD     => $cf->Id,
675                    OPERATOR        => $op,
676                    VALUE           => $args{Term},
677                    ENTRYAGGREGATOR => 'OR',
678                    SUBCLAUSE       => 'autocomplete',
679                );
680            } else {
681                RT->Logger->warning("Asked to search custom field $name but unable to load a User CF with the name $cfname: $msg");
682            }
683        } else {
684            $self->Limit(
685                FIELD           => $name,
686                OPERATOR        => $op,
687                VALUE           => $args{Term},
688                ENTRYAGGREGATOR => 'OR',
689                SUBCLAUSE       => 'autocomplete',
690            );
691        }
692    }
693
694    # Exclude users we don't want
695    $self->Limit(FIELD => 'id', OPERATOR => 'NOT IN', VALUE => $args{Exclude} )
696        if @{$args{Exclude}};
697
698    if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
699        $self->Limit(
700            FIELD    => $args{Return},
701            OPERATOR => 'IS NOT',
702            VALUE    => 'NULL',
703        );
704    }
705    else {
706        $self->Limit( FIELD => $args{Return}, OPERATOR => '!=', VALUE => '' );
707        $self->Limit(
708            FIELD           => $args{Return},
709            OPERATOR        => 'IS NOT',
710            VALUE           => 'NULL',
711            ENTRYAGGREGATOR => 'AND'
712        );
713    }
714
715    return $self;
716}
717
718RT::Base->_ImportOverlays();
719
7201;
721