X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FPrincipal_Overlay.pm;h=c3112595b0daf41e6c53fe3bf2cf781274411003;hp=3e2edaac63d1e901b60b9c6e52de6427b6bb9632;hb=8103c1fc1b2c27a6855feadf26f91b980a54bc52;hpb=d39d52aac8f38ea9115628039f0df5aa3ac826de diff --git a/rt/lib/RT/Principal_Overlay.pm b/rt/lib/RT/Principal_Overlay.pm index 3e2edaac6..c3112595b 100644 --- a/rt/lib/RT/Principal_Overlay.pm +++ b/rt/lib/RT/Principal_Overlay.pm @@ -1,8 +1,8 @@ -# {{{ BEGIN BPS TAGGED BLOCK +# BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2004 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC # # # (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: @@ -42,15 +44,27 @@ # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. # -# }}} END BPS TAGGED BLOCK +# END BPS TAGGED BLOCK }}} +# + +package RT::Principal; + use strict; +use warnings; no warnings qw(redefine); -use vars qw(%_ACL_KEY_CACHE); + +use Cache::Simple::TimedExpiry; + + use RT::Group; use RT::User; +# Set up the ACL cache on startup +our $_ACL_CACHE; +InvalidateACLCache(); + # {{{ IsGroup =head2 IsGroup @@ -131,6 +145,11 @@ sub Object { A helper function which calls RT::ACE->Create + + + Returns a tuple of (STATUS, MESSAGE); If the call succeeded, STATUS is true. Otherwise it's + false. + =cut sub GrantRight { @@ -140,11 +159,6 @@ sub GrantRight { @_); - #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; - } - unless ($args{'Right'}) { return(0, $self->loc("Invalid Right")); } @@ -172,6 +186,11 @@ 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 { @@ -206,7 +225,40 @@ sub RevokeRight { # }}} +# {{{ 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. + +sub _CleanupInvalidDelegations { + my $self = shift; + unless ( $self->Id ) { + $RT::Logger->warning("Principal not loaded."); + return (undef); + } + return ($self->Object->_CleanupInvalidDelegations(@_)); +} +# }}} # {{{ sub HasRight @@ -230,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. @@ -241,270 +291,231 @@ 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") ); + $args{'EquivObjects'} = [ @{ $args{'EquivObjects'} } ] + if $args{'EquivObjects'}; + + if ( $self->Disabled ) { + $RT::Logger->error( "Disabled User #" + . $self->id + . " failed access check for " + . $args{'Right'} ); return (undef); } - if ( defined( $args{'Object'} )) { - return (undef) unless (UNIVERSAL::can( $args{'Object'}, 'id' ) ); - push(@{$args{'EquivObjects'}}, $args{Object}); - } - 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 + 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); + unshift @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj; } + 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); - } - # }}} - - # {{{ 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"); + # {{{ If we've cached a win or loss for this lookup say so - return (undef); + # 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; } - # }}} - - # }}} - - - - # {{{ 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; - - 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')"; - } - - - # }}} - - # {{{ 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).") "; - - - - # The groups query does the query based on group membership and individual user rights - - my $groups_query = $query_base . - - # 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'))". - - " ) "; - $self->_Handle->ApplyLimits(\$groups_query, 1); #only return one result - - my @roles; - foreach my $object (@{$args{'EquivObjects'}}) { - push (@roles, $self->_RolesForObject(ref($object), $object->id)); + { + my $cached_answer = $_ACL_CACHE->fetch($full_hashkey); + return $cached_answer > 0 if defined $cached_answer; } - # 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 ($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); +} - # }}} +=head2 _HasRight - # {{{ Actually check the ACL by performing an SQL query - # $RT::Logger->debug("Now Trying $groups_query"); - my $hitcount = $self->_Handle->FetchResult($groups_query); +Low level HasRight implementation, use HasRight method instead. - # }}} - - # {{{ if there's a match, the right is granted - if ($hitcount) { +=cut - # Cache a positive hit. - $self->_ACLCache->{"$hashkey"}{'set'} = time; - $self->_ACLCache->{"$hashkey"}{'val'} = 1; - return (1); +sub _HasRight +{ + my $self = shift; + { + my ($hit, @other) = $self->_HasGroupRight( @_ ); + return ($hit, @other) if $hit; } - # }}} - # {{{ If there's no match on groups, try it on roles - else { - - $hitcount = $self->_Handle->FetchResult($roles_query); - - if ($hitcount) { - - # Cache a positive hit. - $self->_ACLCache->{"$hashkey"}{'set'} = time; - $self->_ACLCache->{"$hashkey"}{'val'} = 1; - return (1); - } - - else { - # cache a negative hit - $self->_ACLCache->{"$hashkey"}{'set'} = time; - $self->_ACLCache->{"$hashkey"}{'val'} = -1; - - return (undef); - } + { + my ($hit, @other) = $self->_HasRoleRight( @_ ); + return ($hit, @other) if $hit; } - # }}} + return (0); } -# }}} - -# {{{ _RolesForObject - - - -=head2 _RolesForObject( $object_type, $object_id) +# 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'}; -Returns an SQL clause finding role groups for Objects + 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 ) .")"; + } -=cut + $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 _RolesForObject { +sub _HasRoleRight +{ my $self = shift; - my $type = shift; - my $id = shift; - - unless ($id) { - $id = '0'; - } - - # 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 %args = ( + Right => undef, + EquivObjects => [], + @_ + ); + my $right = $args{'Right'}; - my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = $id) "; + my $query = + "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 things + . "AND 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 ACL.PrincipalType = Groups.Type "; + + 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 = "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 ) .")"; + + # 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; } # }}} @@ -513,34 +524,19 @@ 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 - -=cut - -sub _ACLCache { - return(\%_ACL_KEY_CACHE); -} -# }}} +# {{{ InvalidateACLCache -# {{{ _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(); + $_ACL_CACHE->expire_after($RT::ACLCacheLifetime||60); + } # }}} @@ -589,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); }