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