import rt 3.8.10
[freeside.git] / rt / lib / RT / Principal_Overlay.pm
index d2782b7..88e721e 100644 (file)
@@ -1,34 +1,69 @@
-# BEGIN LICENSE BLOCK
-# 
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
-# 
-# (Except where explictly superceded by other copyright notices)
-# 
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
 # from www.gnu.org.
-# 
+#
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
-# 
-# Unless otherwise specified, all modifications, corrections or
-# extensions to this work which alter its source code become the
-# property of Best Practical Solutions, LLC when submitted for
-# inclusion in the work.
-# 
-# 
-# END LICENSE BLOCK
+#
+# 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., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+#
+
+package RT::Principal;
+
 use strict;
+use warnings;
+
+use Cache::Simple::TimedExpiry;
 
-no warnings qw(redefine);
-use vars qw(%_ACL_KEY_CACHE);
 
+use RT;
 use RT::Group;
 use RT::User;
 
+# Set up the ACL cache on startup
+our $_ACL_CACHE;
+InvalidateACLCache();
+
 # {{{ IsGroup
 
 =head2 IsGroup
@@ -40,12 +75,11 @@ Returns undef, otherwise
 
 sub IsGroup {
     my $self = shift;
-    if ($self->PrincipalType eq 'Group') {
-        return(1);
-    }
-    else {
-        return undef;
+    if ( defined $self->PrincipalType && 
+            $self->PrincipalType eq 'Group' ) {
+        return 1;
     }
+    return undef;
 }
 
 # }}}
@@ -82,18 +116,18 @@ Returns the user or group associated with this principal
 sub Object {
     my $self = shift;
 
-    unless ($self->{'object'}) {
-    if ($self->IsUser) {
-       $self->{'object'} = RT::User->new($self->CurrentUser);
-    }
-    elsif ($self->IsGroup) {
-        $self->{'object'}  = RT::Group->new($self->CurrentUser);
-    }
-    else { 
-        $RT::Logger->crit("Found a principal (".$self->Id.") that was neither a user nor a group");
-        return(undef);
-    }
-    $self->{'object'}->Load($self->ObjectId());
+    unless ( $self->{'object'} ) {
+        if ( $self->IsUser ) {
+           $self->{'object'} = RT::User->new($self->CurrentUser);
+        }
+        elsif ( $self->IsGroup ) {
+            $self->{'object'}  = RT::Group->new($self->CurrentUser);
+        }
+        else { 
+            $RT::Logger->crit("Found a principal (".$self->Id.") that was neither a user nor a group");
+            return(undef);
+        }
+        $self->{'object'}->Load( $self->ObjectId() );
     }
     return ($self->{'object'});
 
@@ -109,38 +143,34 @@ sub Object {
 
 A helper function which calls RT::ACE->Create
 
-=cut
-
-sub GrantRight {
-    my $self = shift;
-    my %args = ( Right => undef,
-                Object => undef,
-                @_);
 
 
-    #if we haven't specified any sort of right, we're talking about a global right
-    if (!defined $args{'Object'} && !defined $args{'ObjectId'} && !defined $args{'ObjectType'}) {
-        $args{'Object'} = $RT::System;
-    }
+   Returns a tuple of (STATUS, MESSAGE);  If the call succeeded, STATUS is true. Otherwise it's 
+   false.
 
-    unless ($args{'Right'}) {
-        return(0, $self->loc("Invalid Right"));
-    }
+=cut
 
+sub GrantRight {
+    my $self = shift;
+    my %args = (
+        Right => undef,
+        Object => undef,
+        @_
+    );
 
     #ACL check handled in ACE.pm
     my $ace = RT::ACE->new( $self->CurrentUser );
 
-
     my $type = $self->_GetPrincipalTypeForACL();
 
     # If it's a user, we really want to grant the right to their 
     # user equivalence group
-        return ( $ace->Create(RightName => $args{'Right'},
-                          Object => $args{'Object'},
-                          PrincipalType =>  $type,
-                          PrincipalId => $self->Id
-                          ) );
+    return $ace->Create(
+        RightName     => $args{'Right'},
+        Object        => $args{'Object'},
+        PrincipalType => $type,
+        PrincipalId   => $self->Id,
+    );
 }
 # }}}
 
@@ -150,13 +180,18 @@ sub GrantRight {
 
 Delete a right that a user has 
 
+
+   Returns a tuple of (STATUS, MESSAGE);  If the call succeeded, STATUS is true. Otherwise it's 
+      false.
+
+
 =cut
 
 sub RevokeRight {
 
     my $self = shift;
     my %args = (
-        Right      => undef,
+        Right  => undef,
         Object => undef,
         @_
     );
@@ -169,22 +204,56 @@ sub RevokeRight {
     my $type = $self->_GetPrincipalTypeForACL();
 
     my $ace = RT::ACE->new( $self->CurrentUser );
-    $ace->LoadByValues(
+    my ($status, $msg) = $ace->LoadByValues(
         RightName     => $args{'Right'},
-        Object    => $args{'Object'},
+        Object        => $args{'Object'},
         PrincipalType => $type,
         PrincipalId   => $self->Id
     );
-
-    unless ( $ace->Id ) {
-        return ( 0, $self->loc("ACE not found") );
-    }
-    return ( $ace->Delete );
+    return ($status, $msg) unless $status;
+    return $ace->Delete;
 }
 
 # }}}
 
+# {{{ sub CleanupInvalidDelegations
+
+=head2 sub CleanupInvalidDelegations { InsideTransaction => undef }
+
+Revokes all ACE entries delegated by this principal which are
+inconsistent with this principal's current delegation rights.  Does
+not perform permission checks, but takes no action and returns success
+if this principal still retains DelegateRights.  Should only ever be
+called from inside the RT library.
+
+If this principal is a group, recursively calls this method on each
+cached user member of itself.
+
+If called from inside a transaction, specify a true value for the
+InsideTransaction parameter.
+
+Returns a true value if the deletion succeeded; returns a false value
+and logs an internal error if the deletion fails (should not happen).
+
+=cut
+
+# This is currently just a stub for the methods of the same name in
+# RT::User and RT::Group.
+
+# backcompat for 3.8.8 and before
+*_CleanupInvalidDelegations = \&CleanupInvalidDelegations;
+
+sub CleanupInvalidDelegations {
+    my $self = shift;
+    unless ( $self->Id ) {
+       $RT::Logger->warning("Principal not loaded.");
+       return (undef);
+    }
+    return ($self->Object->CleanupInvalidDelegations(@_));
+}
+
 
+# }}}
 
 # {{{ sub HasRight
 
@@ -208,8 +277,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.
@@ -219,251 +286,285 @@ Returns undef if no ACE was found.
 sub HasRight {
 
     my $self = shift;
-    my %args = ( Right      => undef,
-                 Object     => undef,
-                 EquivObjects    => undef,
-                 @_ );
+    my %args = (
+        Right        => undef,
+        Object       => undef,
+        EquivObjects => undef,
+        @_,
+    );
 
-    if ( $self->Disabled ) {
-        $RT::Logger->err( "Disabled User:  " . $self->id . " failed access check for " . $args{'Right'} );
+    unless ( $args{'Right'} ) {
+        $RT::Logger->crit("HasRight called without a right");
         return (undef);
     }
 
-    if ( !defined $args{'Right'} ) {
-        require Carp;
-        $RT::Logger->debug( Carp::cluck("HasRight called without a right") );
-        return (undef);
+    my $canonic_name = RT::ACE->CanonicalizeRightName( $args{'Right'} );
+    unless ( $canonic_name ) {
+        $RT::Logger->error("Invalid right. Couldn't canonicalize right '$args{'Right'}'");
+        return undef;
     }
+    $args{'Right'} = $canonic_name;
+
+    $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ]
+        if $args{'EquivObjects'};
 
-    if ( defined( $args{'Object'} )) {
-        return (undef) unless (UNIVERSAL::can( $args{'Object'}, 'id' ) );
-        push(@{$args{'EquivObjects'}}, $args{Object});
+    if ( $self->Disabled ) {
+        $RT::Logger->debug( "Disabled User #"
+              . $self->id
+              . " failed access check for "
+              . $args{'Right'} );
+        return (undef);
     }
-    elsif ( $args{'ObjectId'} && $args{'ObjectType'} ) {
-        $RT::Logger->crit(Carp::cluck("API not supprted"));
+
+    if (   defined( $args{'Object'} )
+        && UNIVERSAL::can( $args{'Object'}, 'id' )
+        && $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) {
-        # 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);
 
-    }
+    unshift @{ $args{'EquivObjects'} }, $args{'Object'}->ACLEquivalenceObjects;
 
+    unshift @{ $args{'EquivObjects'} }, $RT::System
+        unless $self->can('_IsOverrideGlobalACL')
+               && $self->_IsOverrideGlobalACL( $args{'Object'} );
 
     # {{{ If we've cached a win or loss for this lookup say so
 
-    # {{{ 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
-        );
-    };
-    # }}}
-
-    #Anything older than 60 seconds needs to be rechecked
-    my $cache_timeout = ( time - 60 );
-
-    # {{{ if we've cached a positive result for this query, return 1
-    if (    ( defined $self->_ACLCache->{"$hashkey"} )
-         && ( $self->_ACLCache->{"$hashkey"}{'val'} == 1 )
-         && ( defined $self->_ACLCache->{"$hashkey"}{'set'} )
-         && ( $self->_ACLCache->{"$hashkey"}{'set'} > $cache_timeout ) ) {
-
-        #$RT::Logger->debug("Cached ACL win for ".  $args{'Right'}.$args{'Scope'}.  $args{'AppliesTo'}."\n");      
-        return ( 1);
+    # 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;
     }
-    # }}}
-
-    #  {{{ if we've cached a negative result for this query return undef
-    elsif (    ( defined $self->_ACLCache->{"$hashkey"} )
-            && ( $self->_ACLCache->{"$hashkey"}{'val'} == -1 )
-            && ( defined $self->_ACLCache->{"$hashkey"}{'set'} )
-            && ( $self->_ACLCache->{"$hashkey"}{'set'} > $cache_timeout ) ) {
 
-        #$RT::Logger->debug("Cached ACL loss decision for ".  $args{'Right'}.$args{'Scope'}.  $args{'AppliesTo'}."\n");            
-
-        return (undef);
+    {
+        my $cached_answer = $_ACL_CACHE->fetch($full_hashkey);
+        return $cached_answer > 0 if defined $cached_answer;
     }
-    # }}}
-
-    # }}}
-
-
-
-    #  {{{ Out of date docs
-    
-    #   We want to grant the right if:
-
-
-    #    # The user has the right as a member of a system-internal or 
-    #    # user-defined group
-    #
-    #    Find all records from the ACL where they're granted to a group 
-    #    of type "UserDefined" or "System"
-    #    for the object "System or the object "Queue N" and the group we're looking
-    #    at has the recursive member $self->Id
-    #
-    #    # The user has the right based on a role
-    #
-    #    Find all the records from ACL where they're granted to the role "foo"
-    #    for the object "System" or the object "Queue N" and the group we're looking
-    #   at is of domain  ("RT::Queue-Role" and applies to the right queue)
-    #                             or ("RT::Ticket-Role" and applies to the right ticket)
-    #    and the type is the same as the type of the ACL and the group has
-    #    the recursive member $self->Id
-    #
-
-    # }}}
-
-    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});
-
-
-
-    foreach my $obj (@{$args{'EquivObjects'}}) {
-            next unless (UNIVERSAL::can($obj, 'id'));
-            my $type = ref($obj);
-            my $id = $obj->id;
-            push @look_at_objects, "(ACL.ObjectType = '$type' AND ACL.ObjectId = '$id')"; 
-            }
 
-     
-    # }}}
 
-    # {{{ 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') ".
-   # 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 . "' ".
-
-    # Make sure the rights apply to the entire system or to the object in question
-    "AND ( ".join(' OR ', @look_at_objects).") ";
+    my ($hitcount, $via_obj) = $self->_HasRight( %args );
 
+    $_ACL_CACHE->set( $full_hashkey => $hitcount? 1: -1 );
+    $_ACL_CACHE->set( "$self_id;:;$args{'Right'};:;$via_obj" => 1 )
+        if $via_obj && $hitcount;
 
+    return ($hitcount);
+}
 
-    # The groups query does the query based on group membership and individual user rights
+=head2 _HasRight
 
-       my $groups_query = $query_base . 
+Low level HasRight implementation, use HasRight method instead.
 
-    # 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'))".
+=cut
 
-        " ) LIMIT 1";
-        
-    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;
+    }
+    {
+        my ($hit, @other) = $self->_HasRoleRight( @_ );
+        return ($hit, @other) if $hit;
     }
+    return (0);
+}
 
-    # 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') LIMIT 1";
+# 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'};
 
-   }
+    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') "
+
+      # 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'";
+
+        if ( ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id ) {
+            $clause .= " AND ACL.ObjectId = ". $obj->id;
+        }
+
+        push @clauses, "($clause)";
+    }
+    if ( @clauses ) {
+        $query .= " AND (". join( ' OR ', @clauses ) .")";
+    }
 
+    $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 => [],
+        @_
+    );
 
-    # {{{ Actually check the ACL by performing an SQL query
-    #   $RT::Logger->debug("Now Trying $groups_query");        
-    my $hitcount = $self->_Handle->FetchResult($groups_query);
+    my @roles = $self->RolesWithRight( %args );
+    return 0 unless @roles;
 
-    # }}}
-    
-    # {{{ if there's a match, the right is granted 
-    if ($hitcount) {
+    my $right = $args{'Right'};
 
-        # Cache a positive hit.
-        $self->_ACLCache->{"$hashkey"}{'set'} = time;
-        $self->_ACLCache->{"$hashkey"}{'val'} = 1;
-        return (1);
+    my $query =
+      "SELECT Groups.id "
+      . "FROM Groups, Principals, CachedGroupMembers WHERE "
+
+      # Never find disabled things
+      . "Principals.Disabled = 0 "
+      . "AND CachedGroupMembers.Disabled = 0 "
+
+      # We always grant rights to Groups
+      . "AND Principals.id = Groups.id "
+      . "AND Principals.PrincipalType = 'Group' "
+
+      # 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 (". join(' OR ', map "Groups.Type = '$_'", @roles ) .")"
+    ;
+
+    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;
+
+        my $clause = "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.
+        $clause .= " AND Groups.Instance = '$id'" if $id;
+        push @object_clauses, "($clause)";
     }
-    # }}}
-    # {{{ If there's no match on groups, try it on roles
-    else {   
-
-       $hitcount = $self->_Handle->FetchResult($roles_query);
-
-        if ($hitcount) {
+    $query .= " AND (". join( ' OR ', @object_clauses ) .")";
 
-            # Cache a positive hit.
-            $self->_ACLCache->{"$hashkey"}{'set'} = time;
-            $self->_ACLCache->{"$hashkey"}{'val'} = 1;
-            return (1);
-           }
+    $self->_Handle->ApplyLimits( \$query, 1 );
+    my ($hit) = $self->_Handle->FetchResult( $query );
+    return (1) if $hit;
 
-        else {
-            # cache a negative hit
-            $self->_ACLCache->{"$hashkey"}{'set'} = time;
-            $self->_ACLCache->{"$hashkey"}{'val'} = -1;
-
-            return (undef);
-           }
-    }
-    # }}}
+    return 0;
 }
 
-# }}}
-
-# {{{ _RolesForObject
+=head2 RolesWithRight
 
+Returns list with names of roles that have right on
+set of objects. Takes Right, EquiveObjects,
+IncludeSystemRights and IncludeSuperusers arguments.
 
+IncludeSystemRights is true by default, rights
+granted on system level are not accouned when option
+is set to false value.
 
-=head2 _RolesForObject( $object_type, $object_id)
-
-Returns an SQL clause finding role groups for Objects
+IncludeSuperusers is true by default, SuperUser right
+is not checked if it's set to false value.
 
 =cut
 
-
-sub _RolesForObject {
+sub RolesWithRight {
     my $self = shift;
-    my $type = shift;
-    my $id = shift;
-    my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = '" . $id. "') ";
+    my %args = (
+        Right               => undef,
+        IncludeSystemRights => 1,
+        IncludeSuperusers   => 1,
+        EquivObjects        => [],
+        @_
+    );
+    my $right = $args{'Right'};
+
+    my $query =
+        "SELECT DISTINCT PrincipalType FROM ACL"
+        # Only find superuser or rights with the name $right
+        ." WHERE ( RightName = '$right' "
+        # Check SuperUser if we were asked to
+        . ($args{'IncludeSuperusers'}? "OR RightName = 'SuperUser' " : '' )
+        .")"
+        # we need only roles
+        ." AND PrincipalType != 'Group'"
+    ;
+
+    # skip rights granted on system level if we were asked to
+    unless ( $args{'IncludeSystemRights'} ) {
+        $query .= " AND ObjectType != 'RT::System'";
+    }
 
-    return($clause);
+    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;
+
+        my $object_clause = "ObjectType = '$type'";
+        $object_clause   .= " AND ObjectId = $id" if $id;
+        push @object_clauses, "($object_clause)";
+    }
+    # find ACLs that are related to our objects only
+    $query .= " AND (". join( ' OR ', @object_clauses ) .")"
+        if @object_clauses;
+
+    my $dbh = $RT::Handle->dbh;
+    my $roles = $dbh->selectcol_arrayref($query);
+    unless ( $roles ) {
+        $RT::Logger->warning( $dbh->errstr );
+        return ();
+    }
+    return @$roles;
 }
 
 # }}}
@@ -472,34 +573,20 @@ sub _RolesForObject {
 
 # {{{ ACL caching
 
-# {{{ _ACLCache
-
-=head2 _ACLCache
 
-# Function: _ACLCache
-# Type    : private instance
-# Args    : none
-# Lvalue  : hash: ACLCache
-# Desc    : Returns a reference to the Key cache hash
+# {{{ InvalidateACLCache
 
-=cut
-
-sub _ACLCache {
-    return(\%_ACL_KEY_CACHE);
-}
-
-# }}}
-
-# {{{ _InvalidateACLCache
+=head2 InvalidateACLCache
 
-=head2 _InvalidateACLCache
-
-Cleans out and reinitializes the user rights key cache
+Cleans out and reinitializes the user rights cache
 
 =cut
 
-sub _InvalidateACLCache {
-    %_ACL_KEY_CACHE = ();
+sub InvalidateACLCache {
+    $_ACL_CACHE = Cache::Simple::TimedExpiry->new();
+    my $lifetime;
+    $lifetime = $RT::Config->Get('ACLCacheLifetime') if $RT::Config;
+    $_ACL_CACHE->expire_after( $lifetime || 60 );
 }
 
 # }}}
@@ -538,7 +625,7 @@ sub _GetPrincipalTypeForACL {
 Returns a list uniquely representing an object or normal scalar.
 
 For scalars, its string value is returned; for objects that has an
-id() method, its class name and Id are returned as a string seperated by a "-".
+id() method, its class name and Id are returned as a string separated by a "-".
 
 =cut
 
@@ -548,6 +635,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);
 }