import rt 3.6.6
[freeside.git] / rt / lib / RT / Principal_Overlay.pm
index 4783c5c..c311259 100644 (file)
@@ -2,7 +2,7 @@
 # 
 # COPYRIGHT:
 #  
-# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
+# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
 #                                          <jesse@bestpractical.com>
 # 
 # (Except where explicitly superseded by other copyright notices)
@@ -22,7 +22,9 @@
 # 
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/copyleft/gpl.html.
 # 
 # 
 # CONTRIBUTION SUBMISSION POLICY:
@@ -280,8 +282,6 @@ This takes the params:
     Object => an RT style object (->id will get its id)
 
 
-
-
 Returns 1 if a matching ACE was found.
 
 Returns undef if no ACE was found.
@@ -295,220 +295,227 @@ sub HasRight {
         Right        => undef,
         Object       => undef,
         EquivObjects => undef,
-        @_
+        @_,
     );
 
+    unless ( $args{'Right'} ) {
+        $RT::Logger->crit("HasRight called without a right");
+        return (undef);
+    }
+
+    $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ]
+        if $args{'EquivObjects'};
+
     if ( $self->Disabled ) {
-        $RT::Logger->err( "Disabled User:  "
+        $RT::Logger->error( "Disabled User #"
               . $self->id
               . " failed access check for "
               . $args{'Right'} );
         return (undef);
     }
 
-    if ( !defined $args{'Right'} ) {
-        $RT::Logger->crit("HasRight called without a right");
-        return (undef);
-    }
-
     if (   defined( $args{'Object'} )
         && UNIVERSAL::can( $args{'Object'}, 'id' )
-        && $args{'Object'}->id )
-    {
-        push( @{ $args{'EquivObjects'} }, $args{Object} );
+        && $args{'Object'}->id ) {
+
+        push @{ $args{'EquivObjects'} }, $args{'Object'};
     }
     else {
-        $RT::Logger->crit("$self HasRight called with no valid object");
+        $RT::Logger->crit("HasRight called with no valid object");
         return (undef);
     }
 
     # If this object is a ticket, we care about ticket roles and queue roles
-    if ( ( ref( $args{'Object'} ) eq 'RT::Ticket' ) && $args{'Object'}->Id ) {
+    if ( UNIVERSAL::isa( $args{'Object'} => 'RT::Ticket' ) ) {
 
-# this is a little bit hacky, but basically, now that we've done the ticket roles magic, we load the queue object
-# and ask all the rest of our questions about the queue.
-        push( @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj );
+        # this is a little bit hacky, but basically, now that we've done
+        # the ticket roles magic, we load the queue object
+        # and ask all the rest of our questions about the queue.
+        unshift @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj;
 
     }
 
-    # {{{ If we've cached a win or loss for this lookup say so
+    unshift @{ $args{'EquivObjects'} }, $RT::System
+        unless $self->can('_IsOverrideGlobalACL')
+               && $self->_IsOverrideGlobalACL( $args{'Object'} );
 
-    # {{{ Construct a hashkey to cache decisions in
-    my $hashkey = do {
-        no warnings 'uninitialized';
-
-        # We don't worry about the hash ordering, as this is only
-        # temporarily used; also if the key changes it would be
-        # invalidated anyway.
-        join(
-            ";:;",
-            $self->Id,
-            map {
-                $_,    # the key of each arguments
-                  ( $_ eq 'EquivObjects' )    # for object arrayref...
-                  ? map( _ReferenceId($_), @{ $args{$_} } )    # calculate each
-                  : _ReferenceId( $args{$_} )    # otherwise just the value
-              } keys %args
-        );
-    };
-
-    # }}}
-
-    # {{{ if we've cached a positive result for this query, return 1
-
-    my $cached_answer = $_ACL_CACHE->fetch($hashkey);
-
-    # Returns undef on cache miss
-    if ( defined $cached_answer ) {
-        if ( $cached_answer == 1 ) {
-            return (1);
-        }
-        elsif ( $cached_answer == -1 ) {
-            return (0);
-        }
-    }
 
-    my ( $or_look_at_object_rights, $or_check_roles );
-    my $right = $args{'Right'};
-
-    # {{{ Construct Right Match
-
-    # If an object is defined, we want to look at rights for that object
-
-    my @look_at_objects;
-    push( @look_at_objects, "ACL.ObjectType = 'RT::System'" )
-      unless $self->can('_IsOverrideGlobalACL')
-      and $self->_IsOverrideGlobalACL( $args{Object} );
+    # {{{ If we've cached a win or loss for this lookup say so
 
-    foreach my $obj ( @{ $args{'EquivObjects'} } ) {
-        next unless ( UNIVERSAL::can( $obj, 'id' ) );
-        my $type = ref($obj);
-        my $id   = $obj->id;
-
-        unless ($id) {
-            use Carp;
-            Carp::cluck(
-                "Trying to check $type rights for an unspecified $type");
-            $RT::Logger->crit(
-                "Trying to check $type rights for an unspecified $type");
-        }
-        push @look_at_objects,
-          "(ACL.ObjectType = '$type' AND ACL.ObjectId = '$id')";
+    # Construct a hashkeys to cache decisions:
+    # 1) full_hashkey - key for any result and for full combination of uid, right and objects
+    # 2) short_hashkey - one key for each object to store positive results only, it applies
+    # only to direct group rights and partly to role rights
+    my $self_id = $self->id;
+    my $full_hashkey = join ";:;", $self_id, $args{'Right'};
+    foreach ( @{ $args{'EquivObjects'} } ) {
+        my $ref_id = _ReferenceId($_);
+        $full_hashkey .= ";:;$ref_id";
+
+        my $short_hashkey = join ";:;", $self_id, $args{'Right'}, $ref_id;
+        my $cached_answer = $_ACL_CACHE->fetch($short_hashkey);
+        return $cached_answer > 0 if defined $cached_answer;
     }
 
-    # }}}
-
-    # {{{ Build that honkin-big SQL query
-
-    my $query_base =
-      "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE  " .
-
-      # Only find superuser or rights with the name $right
-      "(ACL.RightName = 'SuperUser' OR  ACL.RightName = '$right') " .
+    {
+        my $cached_answer = $_ACL_CACHE->fetch($full_hashkey);
+        return $cached_answer > 0 if defined $cached_answer;
+    }
 
-      # Never find disabled groups.
-      "AND Principals.Disabled = 0 "
-      . "AND CachedGroupMembers.Disabled = 0  "
-      . "AND Principals.id = Groups.id "
-      .    # We always grant rights to Groups
 
-# See if the principal is a member of the group recursively or _is the rightholder_
-# never find recursively disabled group members
-# also, check to see if the right is being granted _directly_ to this principal,
-#  as is the case when we want to look up group rights
-"AND  Principals.id = CachedGroupMembers.GroupId AND CachedGroupMembers.MemberId = '"
-      . $self->Id . "' "
-      .
+    my ($hitcount, $via_obj) = $self->_HasRight( %args );
 
-  # Make sure the rights apply to the entire system or to the object in question
-      "AND ( " . join( ' OR ', @look_at_objects ) . ") ";
+    $_ACL_CACHE->set( $full_hashkey => $hitcount? 1: -1 );
+    $_ACL_CACHE->set( "$self_id;:;$args{'Right'};:;$via_obj" => 1 )
+        if $via_obj && $hitcount;
 
-# The groups query does the query based on group membership and individual user rights
+    return ($hitcount);
+}
 
-    my $groups_query = $query_base .
+=head2 _HasRight
 
-# limit the result set to groups of types ACLEquivalence (user)  UserDefined, SystemInternal and Personal
-"AND ( (  ACL.PrincipalId = Principals.id AND ACL.PrincipalType = 'Group' AND "
-      . "(Groups.Domain = 'SystemInternal' OR Groups.Domain = 'UserDefined' OR Groups.Domain = 'ACLEquivalence' OR Groups.Domain = 'Personal'))"
-      .
+Low level HasRight implementation, use HasRight method instead.
 
-      " ) ";
-    $self->_Handle->ApplyLimits( \$groups_query, 1 );    #only return one result
+=cut
 
-    my @roles;
-    foreach my $object ( @{ $args{'EquivObjects'} } ) {
-        push( @roles, $self->_RolesForObject( ref($object), $object->id ) );
+sub _HasRight
+{
+    my $self = shift;
+    {
+        my ($hit, @other) = $self->_HasGroupRight( @_ );
+        return ($hit, @other) if $hit;
     }
-
-    # The roles query does the query based on roles
-    my $roles_query;
-    if (@roles) {
-        $roles_query =
-            $query_base . "AND " . " ( ("
-          . join( ' OR ', @roles ) . " ) "
-          . " AND Groups.Type = ACL.PrincipalType AND Groups.Id = Principals.id AND Principals.PrincipalType = 'Group') ";
-        $self->_Handle->ApplyLimits( \$roles_query, 1 ); #only return one result
-
+    {
+        my ($hit, @other) = $self->_HasRoleRight( @_ );
+        return ($hit, @other) if $hit;
     }
+    return (0);
+}
 
-    # }}}
+# this method handles role rights partly in situations
+# where user plays role X on an object and as well the right is
+# assigned to this role X of the object, for example right CommentOnTicket
+# is granted to Cc role of a queue and user is in cc list of the queue
+sub _HasGroupRight
+{
+    my $self = shift;
+    my %args = (
+        Right        => undef,
+        EquivObjects => [],
+        @_
+    );
+    my $right = $args{'Right'};
 
-    # {{{ Actually check the ACL by performing an SQL query
-    #   $RT::Logger->debug("Now Trying $groups_query");
-    my $hitcount = $self->_Handle->FetchResult($groups_query);
+    my $query =
+      "SELECT ACL.id, ACL.ObjectType, ACL.ObjectId " .
+      "FROM ACL, Principals, CachedGroupMembers WHERE " .
 
-    # }}}
+      # Only find superuser or rights with the name $right
+      "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
 
-    # {{{ if there's a match, the right is granted
-    if ($hitcount) {
-        $_ACL_CACHE->set( $hashkey => 1 );
-        return (1);
-    }
+      # Never find disabled groups.
+      . "AND Principals.id = ACL.PrincipalId "
+      . "AND Principals.PrincipalType = 'Group' "
+      . "AND Principals.Disabled = 0 "
+
+      # See if the principal is a member of the group recursively or _is the rightholder_
+      # never find recursively disabled group members
+      # also, check to see if the right is being granted _directly_ to this principal,
+      #  as is the case when we want to look up group rights
+      . "AND CachedGroupMembers.GroupId  = ACL.PrincipalId "
+      . "AND CachedGroupMembers.GroupId  = Principals.id "
+      . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
+      . "AND CachedGroupMembers.Disabled = 0 ";
+
+    my @clauses;
+    foreach my $obj ( @{ $args{'EquivObjects'} } ) {
+        my $type = ref( $obj ) || $obj;
+        my $clause = "ACL.ObjectType = '$type'";
 
-    # Now check the roles query
-    $hitcount = $self->_Handle->FetchResult($roles_query);
+        if ( ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id ) {
+            $clause .= " AND ACL.ObjectId = ". $obj->id;
+        }
 
-    if ($hitcount) {
-        $_ACL_CACHE->set( $hashkey => 1 );
-        return (1);
+        push @clauses, "($clause)";
+    }
+    if ( @clauses ) {
+        $query .= " AND (". join( ' OR ', @clauses ) .")";
     }
 
-    # We failed to find an acl hit
-    $_ACL_CACHE->set( $hashkey => -1 );
-    return (undef);
-}
-
-# }}}
-
-# {{{ _RolesForObject
+    $self->_Handle->ApplyLimits( \$query, 1 );
+    my ($hit, $obj, $id) = $self->_Handle->FetchResult( $query );
+    return (0) unless $hit;
 
+    $obj .= "-$id" if $id;
+    return (1, $obj);
+}
 
+sub _HasRoleRight
+{
+    my $self = shift;
+    my %args = (
+        Right        => undef,
+        EquivObjects => [],
+        @_
+    );
+    my $right = $args{'Right'};
 
-=head2 _RolesForObject( $object_type, $object_id)
+    my $query =
+      "SELECT ACL.id " .
+      "FROM ACL, Groups, Principals, CachedGroupMembers WHERE " .
 
-Returns an SQL clause finding role groups for Objects
+      # Only find superuser or rights with the name $right
+      "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
 
-=cut
+      # Never find disabled things
+      . "AND Principals.Disabled = 0 "
+      . "AND CachedGroupMembers.Disabled = 0 "
 
+      # We always grant rights to Groups
+      . "AND Principals.id = Groups.id "
+      . "AND Principals.PrincipalType = 'Group' "
 
-sub _RolesForObject {
-    my $self = shift;
-    my $type = shift;
-    my $id = shift;
+      # See if the principal is a member of the group recursively or _is the rightholder_
+      # never find recursively disabled group members
+      # also, check to see if the right is being granted _directly_ to this principal,
+      #  as is the case when we want to look up group rights
+      . "AND Principals.id = CachedGroupMembers.GroupId "
+      . "AND CachedGroupMembers.MemberId = ". $self->Id ." "
+      . "AND ACL.PrincipalType = Groups.Type ";
 
-    unless ($id) {
-       $id = '0';
-   }
+    my (@object_clauses);
+    foreach my $obj ( @{ $args{'EquivObjects'} } ) {
+        my $type = ref($obj)? ref($obj): $obj;
+        my $id;
+        $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
 
-   # This should never be true.
-   unless ($id =~ /^\d+$/) {
-       $RT::Logger->crit("RT::Prinicipal::_RolesForObject called with type $type and a non-integer id: '$id'");
-       $id = "'$id'";
-   }
+        my $object_clause = "ACL.ObjectType = '$type'";
+        $object_clause   .= " AND ACL.ObjectId = $id" if $id;
+        push @object_clauses, "($object_clause)";
+    }
+    # find ACLs that are related to our objects only
+    $query .= " AND (". join( ' OR ', @object_clauses ) .")";
 
-    my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = $id) ";
+    # because of mysql bug in versions up to 5.0.45 we do one query per object
+    # each query should be faster on any DB as it uses indexes more effective
+    foreach my $obj ( @{ $args{'EquivObjects'} } ) {
+        my $type = ref($obj)? ref($obj): $obj;
+        my $id;
+        $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
+
+        my $tmp = $query;
+        $tmp .= " AND Groups.Domain = '$type-Role'";
+        # XXX: Groups.Instance is VARCHAR in DB, we should quote value
+        # if we want mysql 4.0 use indexes here. we MUST convert that
+        # field to integer and drop this quotes.
+        $tmp .= " AND Groups.Instance = '$id'" if $id;
+
+        $self->_Handle->ApplyLimits( \$tmp, 1 );
+        my ($hit) = $self->_Handle->FetchResult( $tmp );
+        return (1) if $hit;
+    }
 
-    return($clause);
+    return 0;
 }
 
 # }}}
@@ -578,6 +585,8 @@ sub _ReferenceId {
     # just return the value for non-objects
     return $scalar unless UNIVERSAL::can($scalar, 'id');
 
+    return ref($scalar) unless $scalar->id;
+
     # an object -- return the class and id
     return(ref($scalar)."-". $scalar->id);
 }