-# {{{ 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-2005 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
# 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
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 {
@_);
- #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"));
}
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 {
# }}}
+# {{{ 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
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.
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);
+ push( @{ $args{'EquivObjects'} }, $args{'Object'}->QueueObj );
}
-
# {{{ 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
+ 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");
-
- return (undef);
+ # Returns undef on cache miss
+ my $cached_answer = $_ACL_CACHE->fetch($hashkey);
+ if ( defined $cached_answer ) {
+ if ( $cached_answer == 1 ) {
+ return (1);
+ }
+ elsif ( $cached_answer == -1 ) {
+ return (undef);
+ }
}
- # }}}
-
- # }}}
+ my $hitcount = $self->_HasRight( %args );
+ $_ACL_CACHE->set( $hashkey => $hitcount? 1:-1 );
+ return ($hitcount);
+}
- # {{{ Out of date docs
-
- # We want to grant the right if:
+=head2 _HasRight
+Low level HasRight implementation, use HasRight method instead.
- # # 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
- #
+=cut
- # }}}
+sub _HasRight
+{
+ my $self = shift;
+ my %args = (
+ Right => undef,
+ Object => undef,
+ EquivObjects => [],
+ @_
+ );
- my ( $or_look_at_object_rights, $or_check_roles );
my $right = $args{'Right'};
-
- # {{{ Construct Right Match
+ my @objects = @{ $args{'EquivObjects'} };
# 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
+ push( @objects, 'RT::System' )
+ unless $self->can('_IsOverrideGlobalACL')
+ && $self->_IsOverrideGlobalACL( $args{Object} );
+
+ my ($check_roles, $check_objects) = ('','');
+ if( @objects ) {
+ my @role_clauses;
+ my @object_clauses;
+ foreach my $obj ( @objects ) {
+ my $type = ref($obj)? ref($obj): $obj;
+ my $id;
+ $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;
+
+ my $role_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.
+ $role_clause .= " AND Groups.Instance = '$id'" if $id;
+ push @role_clauses, "($role_clause)";
+
+ my $object_clause = "ACL.ObjectType = '$type'";
+ $object_clause .= " AND ACL.ObjectId = $id" if $id;
+ push @object_clauses, "($object_clause)";
+ }
+
+ $check_roles .= join ' OR ', @role_clauses;
+ $check_objects = join ' OR ', @object_clauses;
+ }
-
+ my $query_base =
+ "SELECT ACL.id from ACL, Groups, Principals, CachedGroupMembers WHERE " .
- 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
+ # Only find superuser or rights with the name $right
+ "(ACL.RightName = 'SuperUser' OR ACL.RightName = '$right') "
- # 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 . "' ".
+ # Never find disabled groups.
+ . "AND Principals.Disabled = 0 "
+ . "AND CachedGroupMembers.Disabled = 0 "
- # Make sure the rights apply to the entire system or to the object in question
- "AND ( ".join(' OR ', @look_at_objects).") ";
+ # 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 ." "
+ # Make sure the rights apply to the entire system or to the object in question
+ . "AND ($check_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));
- }
-
- # 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
-
- }
-
-
-
- # }}}
-
- # {{{ Actually check the ACL by performing an SQL query
- # $RT::Logger->debug("Now Trying $groups_query");
+ my $groups_query = $query_base
+ # limit the result set to groups of types ACLEquivalence (user),
+ # UserDefined, SystemInternal and Personal. All this we do
+ # via (ACL.PrincipalType = 'Group') condition
+ . "AND ACL.PrincipalId = Principals.id "
+ . "AND ACL.PrincipalType = 'Group' ";
+
+ $self->_Handle->ApplyLimits( \$groups_query, 1 ); #only return one result
my $hitcount = $self->_Handle->FetchResult($groups_query);
+ return 1 if $hitcount; # get out of here if success
- # }}}
-
- # {{{ if there's a match, the right is granted
- if ($hitcount) {
-
- # Cache a positive hit.
- $self->_ACLCache->{"$hashkey"}{'set'} = time;
- $self->_ACLCache->{"$hashkey"}{'val'} = 1;
- return (1);
- }
- # }}}
- # {{{ 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);
- }
- }
- # }}}
-}
-
-# }}}
-
-# {{{ _RolesForObject
-
-
-
-=head2 _RolesForObject( $object_type, $object_id)
-
-Returns an SQL clause finding role groups for Objects
-
-=cut
-
-
-sub _RolesForObject {
- 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'";
- }
+ # The roles query does the query based on roles
+ my $roles_query = $query_base
+ . "AND ACL.PrincipalType = Groups.Type "
+ . "AND ($check_roles) ";
+ $self->_Handle->ApplyLimits( \$roles_query, 1 ); #only return one result
- my $clause = "(Groups.Domain = '".$type."-Role' AND Groups.Instance = $id) ";
+ $hitcount = $self->_Handle->FetchResult($roles_query);
+ return 1 if $hitcount; # get out of here if success
- return($clause);
+ return 0;
}
# }}}
# {{{ 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);
+
}
# }}}
# 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);
}