REALLY require DBIx::SB 1.50, jeez
[freeside.git] / rt / lib / RT / Users_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
6 #                                          <jesse@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/copyleft/gpl.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 =head1 NAME
49
50   RT::Users - Collection of RT::User objects
51
52 =head1 SYNOPSIS
53
54   use RT::Users;
55
56
57 =head1 DESCRIPTION
58
59
60 =head1 METHODS
61
62 =begin testing
63
64 ok(require RT::Users);
65
66 =end testing
67
68 =cut
69
70
71 package RT::Users;
72
73 use strict;
74 no warnings qw(redefine);
75
76 # {{{ sub _Init 
77 sub _Init {
78     my $self = shift;
79     $self->{'table'} = 'Users';
80         $self->{'primary_key'} = 'id';
81
82
83
84     my @result =          $self->SUPER::_Init(@_);
85     # By default, order by name
86     $self->OrderBy( ALIAS => 'main',
87                     FIELD => 'Name',
88                     ORDER => 'ASC' );
89
90     $self->{'princalias'} = $self->NewAlias('Principals');
91
92     # XXX: should be generalized
93     $self->Join( ALIAS1 => 'main',
94                  FIELD1 => 'id',
95                  ALIAS2 => $self->{'princalias'},
96                  FIELD2 => 'id' );
97     $self->Limit( ALIAS => $self->{'princalias'},
98                   FIELD => 'PrincipalType',
99                   VALUE => 'User',
100                 );
101
102     return (@result);
103 }
104
105 # }}}
106
107 =head2 PrincipalsAlias
108
109 Returns the string that represents this Users object's primary "Principals" alias.
110
111 =cut
112
113 # XXX: should be generalized
114 sub PrincipalsAlias {
115     my $self = shift;
116     return($self->{'princalias'});
117
118 }
119
120
121 # {{{ sub _DoSearch 
122
123 =head2 _DoSearch
124
125   A subclass of DBIx::SearchBuilder::_DoSearch that makes sure that _Disabled rows never get seen unless
126 we're explicitly trying to see them.
127
128 =cut
129
130 sub _DoSearch {
131     my $self = shift;
132
133     #unless we really want to find disabled rows, make sure we\'re only finding enabled ones.
134     unless ( $self->{'find_disabled_rows'} ) {
135         $self->LimitToEnabled();
136     }
137     return ( $self->SUPER::_DoSearch(@_) );
138
139 }
140
141 # }}}
142 # {{{ sub LimitToEnabled
143
144 =head2 LimitToEnabled
145
146 Only find items that haven\'t been disabled
147
148 =cut
149
150 # XXX: should be generalized
151 sub LimitToEnabled {
152     my $self = shift;
153
154     $self->Limit( ALIAS    => $self->PrincipalsAlias,
155                   FIELD    => 'Disabled',
156                   VALUE    => '0',
157                   OPERATOR => '=' );
158 }
159
160 # }}}
161
162 # {{{ LimitToEmail
163
164 =head2 LimitToEmail
165
166 Takes one argument. an email address. limits the returned set to
167 that email address
168
169 =cut
170
171 sub LimitToEmail {
172     my $self = shift;
173     my $addr = shift;
174     $self->Limit( FIELD => 'EmailAddress', VALUE => "$addr" );
175 }
176
177 # }}}
178
179 # {{{ MemberOfGroup
180
181 =head2 MemberOfGroup PRINCIPAL_ID
182
183 takes one argument, a group's principal id. Limits the returned set
184 to members of a given group
185
186 =cut
187
188 sub MemberOfGroup {
189     my $self  = shift;
190     my $group = shift;
191
192     return $self->loc("No group specified") if ( !defined $group );
193
194     my $groupalias = $self->NewAlias('CachedGroupMembers');
195
196     # Join the principal to the groups table
197     $self->Join( ALIAS1 => $self->PrincipalsAlias,
198                  FIELD1 => 'id',
199                  ALIAS2 => $groupalias,
200                  FIELD2 => 'MemberId' );
201
202     $self->Limit( ALIAS    => "$groupalias",
203                   FIELD    => 'GroupId',
204                   VALUE    => "$group",
205                   OPERATOR => "=" );
206 }
207
208 # }}}
209
210 # {{{ LimitToPrivileged
211
212 =head2 LimitToPrivileged
213
214 Limits to users who can be made members of ACLs and groups
215
216 =cut
217
218 sub LimitToPrivileged {
219     my $self = shift;
220
221     my $priv = RT::Group->new( $self->CurrentUser );
222     $priv->LoadSystemInternalGroup('Privileged');
223     unless ( $priv->Id ) {
224         $RT::Logger->crit("Couldn't find a privileged users group");
225     }
226     $self->MemberOfGroup( $priv->PrincipalId );
227 }
228
229 # }}}
230
231 # {{{ WhoHaveRight
232
233 =head2 WhoHaveRight { Right => 'name', Object => $rt_object , IncludeSuperusers => undef, IncludeSubgroupMembers => undef, IncludeSystemRights => undef, EquivObjects => [ ] }
234
235 =begin testing
236
237 ok(my $users = RT::Users->new($RT::SystemUser));
238 $users->WhoHaveRight(Object =>$RT::System, Right =>'SuperUser');
239 ok($users->Count == 1, "There is one privileged superuser - Found ". $users->Count );
240 # TODO: this wants more testing
241
242 my $RTxUser = RT::User->new($RT::SystemUser);
243 ($id, $msg) = $RTxUser->Create( Name => 'RTxUser', Comments => "RTx extension user", Privileged => 1);
244 ok ($id,$msg);
245
246 my $group = RT::Group->new($RT::SystemUser);
247 $group->LoadACLEquivalenceGroup($RTxUser->PrincipalObj);
248
249 my $RTxSysObj = {};
250 bless $RTxSysObj, 'RTx::System';
251 *RTx::System::Id = sub { 1; };
252 *RTx::System::id = *RTx::System::Id;
253 my $ace = RT::Record->new($RT::SystemUser);
254 $ace->Table('ACL');
255 $ace->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)});
256 ($id, $msg) = $ace->Create( PrincipalId => $group->id, PrincipalType => 'Group', RightName => 'RTxUserRight', ObjectType => 'RTx::System', ObjectId  => 1 );
257 ok ($id, "ACL for RTxSysObj created");
258
259 my $RTxObj = {};
260 bless $RTxObj, 'RTx::System::Record';
261 *RTx::System::Record::Id = sub { 4; };
262 *RTx::System::Record::id = *RTx::System::Record::Id;
263
264 $users = RT::Users->new($RT::SystemUser);
265 $users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxSysObj);
266 is($users->Count, 1, "RTxUserRight found for RTxSysObj");
267
268 $users = RT::Users->new($RT::SystemUser);
269 $users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj);
270 is($users->Count, 0, "RTxUserRight not found for RTxObj");
271
272 $users = RT::Users->new($RT::SystemUser);
273 $users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj, EquivObjects => [ $RTxSysObj ]);
274 is($users->Count, 1, "RTxUserRight found for RTxObj using EquivObjects");
275
276 $ace = RT::Record->new($RT::SystemUser);
277 $ace->Table('ACL');
278 $ace->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)});
279 ($id, $msg) = $ace->Create( PrincipalId => $group->id, PrincipalType => 'Group', RightName => 'RTxUserRight', ObjectType => 'RTx::System::Record', ObjectId => 5 );
280 ok ($id, "ACL for RTxObj created");
281
282 my $RTxObj2 = {};
283 bless $RTxObj2, 'RTx::System::Record';
284 *RTx::System::Record::Id = sub { 5; };
285 *RTx::System::Record::id = sub { 5; };
286
287 $users = RT::Users->new($RT::SystemUser);
288 $users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj2);
289 is($users->Count, 1, "RTxUserRight found for RTxObj2");
290
291 $users = RT::Users->new($RT::SystemUser);
292 $users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj2, EquivObjects => [ $RTxSysObj ]);
293 is($users->Count, 1, "RTxUserRight found for RTxObj2");
294
295
296 =end testing
297
298 find all users who the right Right for this group, either individually
299 or as members of groups
300
301 If passed a queue object, with no id, it will find users who have that right for _any_ queue
302
303 =cut
304
305 # XXX: should be generalized
306 sub _JoinGroupMembers
307 {
308     my $self = shift;
309     my %args = (
310         IncludeSubgroupMembers => 1,
311         @_
312     );
313
314     my $principals = $self->PrincipalsAlias;
315
316     # The cachedgroupmembers table is used for unrolling group memberships
317     # to allow fast lookups. if we bind to CachedGroupMembers, we'll find
318     # all members of groups recursively. if we don't we'll find only 'direct'
319     # members of the group in question
320     my $group_members;
321     if ( $args{'IncludeSubgroupMembers'} ) {
322         $group_members = $self->NewAlias('CachedGroupMembers');
323     }
324     else {
325         $group_members = $self->NewAlias('GroupMembers');
326     }
327
328     $self->Join(
329         ALIAS1 => $group_members,
330         FIELD1 => 'MemberId',
331         ALIAS2 => $principals,
332         FIELD2 => 'id'
333     );
334
335     return $group_members;
336 }
337
338 # XXX: should be generalized
339 sub _JoinGroups
340 {
341     my $self = shift;
342     my %args = (@_);
343
344     my $group_members = $self->_JoinGroupMembers( %args );
345     my $groups = $self->NewAlias('Groups');
346     $self->Join(
347         ALIAS1 => $groups,
348         FIELD1 => 'id',
349         ALIAS2 => $group_members,
350         FIELD2 => 'GroupId'
351     );
352
353     return $groups;
354 }
355
356 # XXX: should be generalized
357 sub _JoinACL
358 {
359     my $self = shift;
360     my %args = (
361         Right                  => undef,
362         IncludeSuperusers      => undef,
363         @_,
364     );
365
366     my $acl = $self->NewAlias('ACL');
367     $self->Limit(
368         ALIAS    => $acl,
369         FIELD    => 'RightName',
370         OPERATOR => ( $args{Right} ? '=' : 'IS NOT' ),
371         VALUE => $args{Right} || 'NULL',
372         ENTRYAGGREGATOR => 'OR'
373     );
374     if ( $args{'IncludeSuperusers'} and $args{'Right'} ) {
375         $self->Limit(
376             ALIAS           => $acl,
377             FIELD           => 'RightName',
378             OPERATOR        => '=',
379             VALUE           => 'SuperUser',
380             ENTRYAGGREGATOR => 'OR'
381         );
382     }
383     return $acl;
384 }
385
386 # XXX: should be generalized
387 sub _GetEquivObjects
388 {
389     my $self = shift;
390     my %args = (
391         Object                 => undef,
392         IncludeSystemRights    => undef,
393         EquivObjects           => [ ],
394         @_
395     );
396     return () unless $args{'Object'};
397
398     my @objects = ($args{'Object'});
399     if ( UNIVERSAL::isa( $args{'Object'}, 'RT::Ticket' ) ) {
400         # If we're looking at ticket rights, we also want to look at the associated queue rights.
401         # this is a little bit hacky, but basically, now that we've done the ticket roles magic,
402         # we load the queue object and ask all the rest of our questions about the queue.
403
404         # XXX: This should be abstracted into object itself
405         if( $args{'Object'}->id ) {
406             push @objects, $args{'Object'}->QueueObj;
407         } else {
408             push @objects, 'RT::Queue';
409         }
410     }
411
412     if( $args{'IncludeSystemRights'} ) {
413         push @objects, 'RT::System';
414     }
415     push @objects, @{ $args{'EquivObjects'} };
416     return grep $_, @objects;
417 }
418
419 # XXX: should be generalized
420 sub WhoHaveRight {
421     my $self = shift;
422     my %args = (
423         Right                  => undef,
424         Object                 => undef,
425         IncludeSystemRights    => undef,
426         IncludeSuperusers      => undef,
427         IncludeSubgroupMembers => 1,
428         EquivObjects           => [ ],
429         @_
430     );
431
432     if ( defined $args{'ObjectType'} || defined $args{'ObjectId'} ) {
433         $RT::Logger->crit( "WhoHaveRight called with the Obsolete ObjectId/ObjectType API");
434         return (undef);
435     }
436
437     my @from_role = $self->Clone->_WhoHaveRoleRightSplitted( %args );
438
439     my $from_group = $self->Clone;
440     $from_group->WhoHaveGroupRight( %args );
441
442     #XXX: DIRTY HACK
443     use DBIx::SearchBuilder 1.50; #no version on ::Union :(
444     use DBIx::SearchBuilder::Union;
445     my $union = new DBIx::SearchBuilder::Union;
446     $union->add( $_ ) foreach @from_role;
447     $union->add( $from_group );
448     %$self = %$union;
449     bless $self, ref($union);
450
451     return;
452 }
453 # }}}
454
455 # XXX: should be generalized
456 sub WhoHaveRoleRight
457 {
458     my $self = shift;
459     my %args = (
460         Right                  => undef,
461         Object                 => undef,
462         IncludeSystemRights    => undef,
463         IncludeSuperusers      => undef,
464         IncludeSubgroupMembers => 1,
465         EquivObjects           => [ ],
466         @_
467     );
468
469     my $groups = $self->_JoinGroups( %args );
470     my $acl = $self->_JoinACL( %args );
471
472     $self->Limit( ALIAS => $acl,
473                   FIELD => 'PrincipalType',
474                   VALUE => "$groups.Type",
475                   QUOTEVALUE => 0,
476                 );
477
478     # no system user
479     $self->Limit( ALIAS => $self->PrincipalsAlias,
480                   FIELD => 'id',
481                   OPERATOR => '!=',
482                   VALUE => $RT::SystemUser->id
483                 );
484
485     my @objects = $self->_GetEquivObjects( %args );
486     unless ( @objects ) {
487         unless ( $args{'IncludeSystemRights'} ) {
488             $self->_AddSubClause( WhichObjects => "($acl.ObjectType != 'RT::System')" );
489         }
490         return;
491     }
492
493     my ($groups_clauses, $acl_clauses) = $self->_RoleClauses( $groups, $acl, @objects );
494     $self->_AddSubClause( "WhichObject", "(". join( ' OR ', @$groups_clauses ) .")" );
495     $self->_AddSubClause( "WhichRole", "(". join( ' OR ', @$acl_clauses ) .")" );
496
497     return;
498 }
499
500 sub _WhoHaveRoleRightSplitted {
501     my $self = shift;
502     my %args = (
503         Right                  => undef,
504         Object                 => undef,
505         IncludeSystemRights    => undef,
506         IncludeSuperusers      => undef,
507         IncludeSubgroupMembers => 1,
508         EquivObjects           => [ ],
509         @_
510     );
511
512     my $groups = $self->_JoinGroups( %args );
513     my $acl = $self->_JoinACL( %args );
514
515     $self->Limit( ALIAS => $acl,
516                   FIELD => 'PrincipalType',
517                   VALUE => "$groups.Type",
518                   QUOTEVALUE => 0,
519                 );
520
521     # no system user
522     $self->Limit( ALIAS => $self->PrincipalsAlias,
523                   FIELD => 'id',
524                   OPERATOR => '!=',
525                   VALUE => $RT::SystemUser->id
526                 );
527
528     my @objects = $self->_GetEquivObjects( %args );
529     unless ( @objects ) {
530         unless ( $args{'IncludeSystemRights'} ) {
531             $self->_AddSubClause( WhichObjects => "($acl.ObjectType != 'RT::System')" );
532         }
533         return $self;
534     }
535
536     my ($groups_clauses, $acl_clauses) = $self->_RoleClauses( $groups, $acl, @objects );
537     $self->_AddSubClause( "WhichRole", "(". join( ' OR ', @$acl_clauses ) .")" );
538     
539     my @res;
540     foreach ( @$groups_clauses ) {
541         my $tmp = $self->Clone;
542         $tmp->_AddSubClause( WhichObject => $_ );
543         push @res, $tmp;
544     }
545
546     return @res;
547 }
548
549 sub _RoleClauses {
550     my $self = shift;
551     my $groups = shift;
552     my $acl = shift;
553     my @objects = @_;
554
555     my @groups_clauses;
556     my @acl_clauses;
557     foreach my $obj ( @objects ) {
558         my $type = ref($obj)? ref($obj): $obj;
559         my $id;
560         $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
561
562         my $role_clause = "$groups.Domain = '$type-Role'";
563         # XXX: Groups.Instance is VARCHAR in DB, we should quote value
564         # if we want mysql 4.0 use indexes here. we MUST convert that
565         # field to integer and drop this quotes.
566         $role_clause   .= " AND $groups.Instance = '$id'" if $id;
567         push @groups_clauses, "($role_clause)";
568
569         my $object_clause = "$acl.ObjectType = '$type'";
570         $object_clause   .= " AND $acl.ObjectId = $id" if $id;
571         push @acl_clauses, "($object_clause)";
572     }
573     return (\@groups_clauses, \@acl_clauses);
574 }
575
576 # XXX: should be generalized
577 sub _JoinGroupMembersForGroupRights
578 {
579     my $self = shift;
580     my %args = (@_);
581     my $group_members = $self->_JoinGroupMembers( %args );
582     $self->Limit( ALIAS => $args{'ACLAlias'},
583                   FIELD => 'PrincipalId',
584                   VALUE => "$group_members.GroupId",
585                   QUOTEVALUE => 0,
586                 );
587 }
588
589 # XXX: should be generalized
590 sub WhoHaveGroupRight
591 {
592     my $self = shift;
593     my %args = (
594         Right                  => undef,
595         Object                 => undef,
596         IncludeSystemRights    => undef,
597         IncludeSuperusers      => undef,
598         IncludeSubgroupMembers => 1,
599         EquivObjects           => [ ],
600         @_
601     );
602
603     # Find only rows where the right granted is
604     # the one we're looking up or _possibly_ superuser
605     my $acl = $self->_JoinACL( %args );
606
607     my ($check_objects) = ('');
608     my @objects = $self->_GetEquivObjects( %args );
609
610     if ( @objects ) {
611         my @object_clauses;
612         foreach my $obj ( @objects ) {
613             my $type = ref($obj)? ref($obj): $obj;
614             my $id;
615             $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
616
617             my $object_clause = "$acl.ObjectType = '$type'";
618             $object_clause   .= " AND $acl.ObjectId   = $id" if $id;
619             push @object_clauses, "($object_clause)";
620         }
621
622         $check_objects = join ' OR ', @object_clauses;
623     } else {
624         if( !$args{'IncludeSystemRights'} ) {
625             $check_objects = "($acl.ObjectType != 'RT::System')";
626         }
627     }
628     $self->_AddSubClause( "WhichObject", "($check_objects)" );
629     
630     $self->_JoinGroupMembersForGroupRights( %args, ACLAlias => $acl );
631     # Find only members of groups that have the right.
632     $self->Limit( ALIAS => $acl,
633                   FIELD => 'PrincipalType',
634                   VALUE => 'Group',
635                 );
636     
637     # no system user
638     $self->Limit( ALIAS => $self->PrincipalsAlias,
639                   FIELD => 'id',
640                   OPERATOR => '!=',
641                   VALUE => $RT::SystemUser->id
642                 );
643     return;
644 }
645
646 # {{{ WhoBelongToGroups
647
648 =head2 WhoBelongToGroups { Groups => ARRAYREF, IncludeSubgroupMembers => 1 }
649
650 =cut
651
652 # XXX: should be generalized
653 sub WhoBelongToGroups {
654     my $self = shift;
655     my %args = ( Groups                 => undef,
656                  IncludeSubgroupMembers => 1,
657                  @_ );
658
659     # Unprivileged users can't be granted real system rights.
660     # is this really the right thing to be saying?
661     $self->LimitToPrivileged();
662
663     my $group_members = $self->_JoinGroupMembers( %args );
664
665     foreach my $groupid (@{$args{'Groups'}}) {
666         $self->Limit( ALIAS           => $group_members,
667                       FIELD           => 'GroupId',
668                       VALUE           => $groupid,
669                       QUOTEVALUE      => 0,
670                       ENTRYAGGREGATOR => 'OR',
671                     );
672     }
673 }
674 # }}}
675
676
677 1;