diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 528f69d45..20d83971c 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -16,6 +16,7 @@ - fixed #11566: Group API: group membership cannot be checked without consideration of expiration dates. - fixed #11567: EMS: Build badge page, ticket tab, pagination - added: a new inbox setting which supresses friend rejection notices + - fixed #11552: Visitors (and others) can bypass group-by-IP restrictions 7.9.4 - We're shipping underscore.js now for its suite of extremely handy utility diff --git a/docs/upgrades/upgrade_7.9.4-7.9.5.pl b/docs/upgrades/upgrade_7.9.4-7.9.5.pl index a39f8dc46..22d7305fa 100644 --- a/docs/upgrades/upgrade_7.9.4-7.9.5.pl +++ b/docs/upgrades/upgrade_7.9.4-7.9.5.pl @@ -35,6 +35,7 @@ my $session = start(); # this line required modifySortItems( $session ); fixRequestForApprovalScratch($session); addRejectNoticeSetting($session); +updateGroupGroupingsTable($session); finish($session); # this line required @@ -56,6 +57,16 @@ sub addRejectNoticeSetting { print "DONE!\n" unless $quiet; } +#---------------------------------------------------------------------------- +# Add keys and indicies to groupGroupings to help speed up group queries +sub updateGroupGroupingsTable { + my $session = shift; + print "\tAdding primary key and indicies to groupGroupings table... " unless $quiet; + $session->db->write("alter table groupGroupings add primary key (groupId,inGroup)"); + $session->db->write("alter table groupGroupings add index inGroup (inGroup)"); + print "DONE!\n" unless $quiet; +} + #---------------------------------------------------------------------------- # Describe what our function does sub fixRequestForApprovalScratch { diff --git a/lib/WebGUI/Group.pm b/lib/WebGUI/Group.pm index 00e53c32a..8d675f153 100644 --- a/lib/WebGUI/Group.pm +++ b/lib/WebGUI/Group.pm @@ -219,6 +219,47 @@ sub autoDelete { return $self->get("autoDelete"); } +#------------------------------------------------------------------- + +=head2 cacheGroupings ( user, is_member ) + +Adds a record to the grouping for this group into the cache. + +=head3 user + +User object to set cache for + +=head3 is_member + +Boolean which indicates whether or not the user passed in is a member of this group + +=cut + +sub cacheGroupings { + my $self = shift; + my $session = $self->session; + my $groupId = $self->getId; + my $user = shift; + my $isInGroup = shift || 0; + my $userId = $user->userId; + my $sessionId = $session->getId; + + ### Undocumented - cache and groupMembers can be passed in if it they are already built. + #These exist specifically for WebGUI::User::isInGroup to use and should not be used elsewhere + #unless you know what you are doing + my $cache = shift || WebGUI::Cache->new($session,["groupMembers",$groupId]) || {}; + my $groupMembers = shift || $cache->get; + + #Build cache in a special way for visitors + if($userId eq '1') { + $groupMembers->{$userId}->{$sessionId} = { isMember => $isInGroup }; + } + else { + $groupMembers->{$userId} = { isMember => $isInGroup }; + } + + $cache->set($groupMembers, $self->groupCacheTimeout); +} #------------------------------------------------------------------- @@ -229,15 +270,17 @@ Clears all caches for this group and any ancestor groups of the group. =cut sub clearCaches { - my $self = shift; + my $self = shift; + my $session = $self->session; ##Clear my cache and the cache of all groups above me. my $groups = $self->getAllGroupsFor(); - foreach my $group ( $self->getId, @{ $groups } ) { - WebGUI::Cache->new($self->session, $group)->delete; + foreach my $groupId ( $self->getId, @{ $groups } ) { + WebGUI::Cache->new($session, $groupId)->delete; + WebGUI::Cache->new($session, ["groupMembers", $groupId])->delete; } - $self->session->stow->delete("groupObj"); - $self->session->stow->delete("isInGroup"); - $self->session->stow->delete("gotGroupsInGroup"); + $session->stow->delete("groupObj"); + $session->stow->delete("isInGroup"); + $session->stow->delete("gotGroupsInGroup"); } #------------------------------------------------------------------- @@ -523,7 +566,7 @@ sub get { =head2 getAllGroupsFor ( ) -Returns an array reference containing a list of all groups this group is in, recursively. +Returns an array reference containing a list of all groupIds this group is in, recursively. =cut @@ -637,7 +680,7 @@ sub getDatabaseUsers { =head2 getGroupsFor ( ) -Returns an array reference containing a list of groups this group is in. This method +Returns an array reference containing a list of groupIds this group is in. This method does not check recursively backwards up the list of groups. =cut @@ -711,7 +754,7 @@ Returns the groupId for this group. sub getId { my $self = shift; - return $self->{_groupId}; + return $self->{_groupId}; } @@ -956,6 +999,373 @@ sub getUsersNotIn { } +#------------------------------------------------------------------- + +=head2 hasDatabaseUser ( userId ) + +Determine if the user passed in is a member of this group via a database query. + +Membership will always be false if no query or database link has been defined +for this group. + +=head3 userId + +id of the user to check for membership + +=cut + +sub hasDatabaseUser { + my $self = shift; + my $userId = shift; + my $session = $self->session; + my $gid = $self->getId; + + my $query = $self->get("dbQuery"); + my $dbLinkId = $self->get("databaseLinkId"); + return 0 unless ($userId && $query && defined $dbLinkId); + + my $dbLink = WebGUI::DatabaseLink->new($session,$dbLinkId); + unless (defined $dbLink) { + $session->log->error("The database link ".$dbLinkId." no longer exists even though group ".$gid." references it. Group $gid may not be working correctly"); + return 0; + } + + my $dbh = $dbLink->db; + unless (defined $dbh) { + $session->log->error("Link to database established by could not get database handler for group $gid. This group may not be working correctly"); + $dbLink->disconnect; + return 0; + } + + WebGUI::Macro::process($self->session,\$query); + #Try to speed up the query by adding a userId filter to the where clause + if ($query =~ m/^\s*SELECT\s*(.*)\s*FROM/i) { + my $uid_ident = $1; + $query =~ s/where/where $uid_ident = '$userId' and/i; + } + my $sth = $dbh->unconditionalRead($query); + + unless(defined $sth) { + $session->log->error("Couldn't process unconditional read for database group with group id $gid. This group may not be working correctly"); + return 0; + } + + unless ($sth->errorCode < 1) { + $session->log->warn("There was a problem with the database query for group ID $gid."); + return 0; + } + + while (my ($uid) = $sth->array) { + if ($uid eq $userId) { + return 1; + } + } + return 0; +} + +#------------------------------------------------------------------- + +=head2 hasIpUser ( userId ) + +Determine if the user passed in is a member of this group via the lastIP recorded +in the user's session and this group's IpFilter. + +Membership will always be false if no IpFilter has been set + +=head3 userId + +id of the user to check for membership + +=cut + +sub hasIpUser { + my $self = shift; + my $userId = shift; + my $session = $self->session; + + my $IpFilter = $self->ipFilter(); + return 0 unless ($IpFilter && $userId); + + $IpFilter =~ s/\s//g; + my @filters = split /;/, $IpFilter; + + my @ips = $session->db->buildArray( + q{ select lastIP from userSession where expires > ? and userId = ? } + ,[ time(), $userId ] + ); + + foreach my $ip (@ips) { + return 1 if (isInSubnet($ip,\@filters)); + } + + return 0; +} + + +#------------------------------------------------------------------- + +=head2 hasKarmaUser ( userId ) + +Determine if the user passed in is a member of this group via the their current +karma setting and this group's karmaThreshold. + +If karma is not enabled for this site, membership will always be false. + +=head3 userId + +id of the user to check for membership + +=cut + +sub hasKarmaUser { + my $self = shift; + my $userId = shift; + my $session = $self->session; + + return 0 unless ($session->setting->get('useKarma') && $userId); + + return $session->db->quickScalar( + q{ select count(*) from users where karma >= ? and userId = ? } + ,[$self->karmaThreshold,$userId] + ); +} + +#------------------------------------------------------------------- + +=head2 hasLDAPUser ( userId ) + +Determine if the user passed in is a member of this group via an LDAP +connection + +If ldapLink, ldapGroup, and ldapGroupProperty are not configured for this group +membership will always be false. + +#TODO - change the way this works to search LDAP for the dn associated with the +userId. That should speed this up a bunch for people using LDAP groups. + +=head3 userId + +id of the user to check for membership + +=cut + +sub hasLDAPUser { + my $self = shift; + my $userId = shift; + my $session = $self->session; + my @ldapUsers = (); + my $gid = $self->getId; + + ### Check LDAP + my $ldapLinkId = $self->get("ldapLinkId"); + my $ldapGroup = $self->get("ldapGroup"); + my $ldapGroupProperty = $self->get("ldapGroupProperty"); + my $ldapRecursiveProperty = $self->get("ldapRecursiveProperty"); + my $ldapRecurseFilter = $self->get("ldapRecursiveFilter"); + + return 0 unless ($ldapLinkId && $ldapGroup && $ldapGroupProperty && $userId); + + my $ldapLink = WebGUI::LDAPLink->new($session,$ldapLinkId); + unless ($ldapLink && $ldapLink->bind) { + $self->session->errorHandler->warn("There was a problem connecting to LDAP link $ldapLinkId for group ID $gid."); + return 0; + } + + my $people = []; + if($ldapRecursiveProperty) { + $ldapLink->recurseProperty($ldapGroup,$people,$ldapGroupProperty,$ldapRecursiveProperty,$ldapRecurseFilter); + } else { + $people = $ldapLink->getProperty($ldapGroup,$ldapGroupProperty); + } + $ldapLink->unbind; + + foreach my $person (@{$people}) { + $person =~ s/\s*,\s*/,/g; + $person = lc($person); + my $personRegExp = "^uid=$person,"; + my $uid = $session->db->quickScalar("select userId from authentication where authMethod='LDAP' and fieldName='connectDN' and lower(fieldData) = ? OR lower(fieldData) REGEXP ?",[$person,$personRegExp]); + return 1 if ($uid eq $userId); + } + + return 0; +} + +#------------------------------------------------------------------- + +=head2 hasScratchUser ( userId ) + +Determine if the user passed in is a member of this group via session scratch +variable settings and this group's scratchFilter. + +If no scratchFilter has been set for this group, membership will always be false. + +=head3 userId + +id of the user to check for membership + +=cut + +sub hasScratchUser { + my $self = shift; + my $userId = shift; + my $session = $self->session; + + my $scratchFilter = $self->scratchFilter(); + return 0 unless ($scratchFilter && $userId); + + $scratchFilter =~ s/\s//g; + my @filters = split /;/, $scratchFilter; + + my @scratchClauses = (); + my @scratchPlaceholders = ( $userId, time() ); + foreach my $filter (@filters) { + my ($name, $value) = split /=/, $filter; + push @scratchClauses, "(s.name=? AND s.value=?)"; + push @scratchPlaceholders, $name, $value; + } + my $scratchClause = join ' OR ', @scratchClauses; + + my $query = qq{ + select + count(*) + from + userSession u, userSessionScratch s + where + u.sessionId=s.sessionId AND + u.userId = ? AND + u.expires > ? AND + ( $scratchClause ) + }; + + return $session->db->quickScalar($query, [ @scratchPlaceholders ]); +} + + +#------------------------------------------------------------------- + +=head2 hasUser ( user ) + +Determine if the user passed in is a member of one of the special groups +for this group + +=head3 user + +user object to check groups for + +=cut + +sub hasUser { + my $self = shift; + my $session = $self->session; + my $user = shift || WebGUI::User->new($session,3); #Check the admin account if no user is passed in + my $gid = $self->getId; + my $db = $session->db; + + my $uid = $user->userId; + ### Get what's in session cache for this group + my $isInGroup = $session->stow->get("isInGroup", { noclone => 1 }) || {}; + ### Check to see that we have a cache built for this user + my $hasCache = (exists $isInGroup->{$uid}->{cached}); + + ### Return what is in the cache if we've already cached this group in the session. + return $isInGroup->{$uid}->{$gid} if ( exists $isInGroup->{$uid}->{$gid} ); + + ### If we dont' have a cache yet, cache all of the groups this user is directly a member of + ### this will only happen if there is no cache built for this user and it saves us from running one query per group + unless ($hasCache) { + ### Get the list of groups this user is directly a member of + my @groups = $db->buildArray( + q{ select groupId from groupings where userId=? and expireDate > ? } + , [$uid,time()] + ); + ### Cache the groupings we find + map { $isInGroup->{$uid}->{$_} = 1 } @groups; + ### Set a cached flag so someone else doesn't accidentally call stow before us and screw our quick caching method + $isInGroup->{$uid}->{cached} = 1; + ### Stow the cache here because we have set the cache for other groups besides this one. + $session->stow->set("isInGroup",$isInGroup); + ### Return if we found the user in this group + return 1 if ( $isInGroup->{$uid}->{$gid} ); + } + + ### User was not found directly in this group. Create a list of groups to check deeply and add this group to that list + my $groupsToCheckDeeply = { $gid => 1 }; + + #Made it here because user is not in the group itself. Now check for direct existance in the sub groups. + #Now build a list of the subgroups for this group that the user is part of + + ### Check all of the sub groups for direct existance, caching all of the subgroups that we do not find the user + ### in our list of groups that need to be checked more deeply + my @groups = ($gid); #Start checking sub groups of this group only + my $loopLimit = 100; #Set a loop limit just to be safe + while (scalar(@groups) && $loopLimit--) { + ### Check all of the groups of groups for all of the current @groups array. The query below + ### returns the group that was in the group along with whether or not the user is directly a member + my $sqlInStr = $db->quoteAndJoin(\@groups); + my $sth = $db->read( + qq{ select + groupGroupings.groupId, userId + from + groupGroupings + left join groupings on groupGroupings.groupId=groupings.groupId and userId=? + where + inGroup in ($sqlInStr) + } + ,[$uid] + ); + ### Create a subgroup cache for this pass of the loop so we know what groups to check next + my $subgroupCache = {}; + while (my ($groupId,$userId) = $sth->array){ + next if ($subgroupCache->{$groupId}); #Skip subgroups we've already checked - nothing has changed + ### Return true if we find that the user is in the sub group from the session cache - no need to stow any caches here + return 1 if ($isInGroup->{$uid}->{$groupId}); + ### If the userId field is not null, that means that this user is directly a member of this sub group + if($userId) { + ### Stow the result and return true; + $isInGroup->{$uid}->{$groupId} = 1; #Cache the sub group results + $isInGroup->{$uid}->{$gid} = 1; #Cache the results for the group we are checking + $session->stow->set("isInGroup",$isInGroup); #Stow the Cache + return 1; + } + ### We made it here because the user is not directly in the subgroup. + $subgroupCache->{$groupId} = 1; #Update the subgroup cache for the next outer loop pass + $groupsToCheckDeeply->{$groupId} = 1; #We need to check this group more deeply + } + ### Get the next level of sub groups to check from the subgroupCache keys. + @groups = keys %{$subgroupCache}; + } + + ### Made it here because the user is not directly in the group itself or directly in any of it's subgroups + ### We should have a flattened list of groups in this group that we should now check one by one to see if the + ### user is is a member via one of the other methods available for groups + + foreach my $groupIdInGroup (keys %{$groupsToCheckDeeply}) { + ### Instantiate the group + my $groupToCheck = __PACKAGE__->new($session,$groupIdInGroup); + ### Check the 'has' method for each of the 'other' group methods available for this user + ### perform checks in a least -> most expensive manner. If we find the user, stow the cache and return true + if( $groupToCheck->hasIpUser($uid) + || $groupToCheck->hasKarmaUser($uid) + || $groupToCheck->hasScratchUser($uid) + || $groupToCheck->hasDatabaseUser($uid) + || $groupToCheck->hasLDAPUser($uid) + ) { + #Found the user in one of the 'other' group methods + $isInGroup->{$uid}->{$groupIdInGroup} = 1; #Cache the results for this group so we don't have to check it again + $isInGroup->{$uid}->{$gid} = 1; #Cache the results for the main group because we found the user in one of the subgroups + $session->stow->set("isInGroup",$isInGroup); #Stow the cache + return 1; + } + #Made it here because we did not find the user at all in this subgroup. Cache the result so we don't have to check this subgroup again. + $isInGroup->{$uid}->{$groupIdInGroup} = 0; + } + + #If we made it here, that means the user is not in the group or any of it's sub groups + #Cache the result, stow the cache, and return false as this group does not contain the user. + $isInGroup->{$uid}->{$gid} = 0; + $session->stow->set("isInGroup",$isInGroup); + return 0; +} #------------------------------------------------------------------- diff --git a/lib/WebGUI/User.pm b/lib/WebGUI/User.pm index 01db752e1..97ea00130 100644 --- a/lib/WebGUI/User.pm +++ b/lib/WebGUI/User.pm @@ -900,7 +900,7 @@ sub isEnabled { #------------------------------------------------------------------- -=head2 isInGroup ( [ groupId ] ) +=head2 isInGroup ( [groupId ] ) Returns a boolean (0|1) value signifying that the user has the required privileges. Always returns true for Admins. @@ -911,38 +911,48 @@ The group that you wish to verify against the user. Defaults to group with Id 3 =cut sub isInGroup { - my ($self, $gid) = @_; - $gid = 3 unless $gid; - my $uid = $self->userId; - ### The following several checks are to increase performance. If this section were removed, everything would continue to work as normal. - #my $eh = $self->session->errorHandler; - #$eh->warn("Group Id is: $gid for ".$tgroup->name); - return 1 if ($gid eq '7'); # everyone is in the everyone group - return 1 if ($gid eq '1' && $uid eq '1'); # visitors are in the visitors group - return 1 if ($gid eq '2' && $uid ne '1'); # if you're not a visitor, then you're a registered user - ### Get data for auxillary checks. - my $isInGroup = $self->session->stow->get("isInGroup", { noclone => 1 }); - ### Look to see if we've already looked up this group. - return $isInGroup->{$uid}{$gid} if exists $isInGroup->{$uid}{$gid}; - ### Lookup the actual groupings. - my $group = WebGUI::Group->new($self->session,$gid); - if ( !$group ) { - $group = WebGUI::Group->new($self->session,3); - } - ### Check for groups of groups. - my $users = $group->getAllUsers(); - foreach my $user (@{$users}) { - $isInGroup->{$user}{$gid} = 1; - if ($uid eq $user) { - $self->session->stow->set("isInGroup",$isInGroup); - return 1; - } - } - $isInGroup->{$uid}{$gid} = 0; - $self->session->stow->set("isInGroup",$isInGroup); - return 0; -} + my ($self, $gid) = @_; + my $session = $self->session; + my $uid = $self->userId; + $gid = 3 unless $gid; + ### The following several checks are to increase performance. If this section were removed, everything would continue to work as normal. + return 1 if ($gid eq '7'); # everyone is in the everyone group + return 1 if ($gid eq '1' && $uid eq '1'); # visitors are in the visitors group + return 1 if ($gid eq '2' && $uid ne '1'); # if you're not a visitor, then you're a registered user + + ### Check stow before we check the cache. Stow is in memory and much faster + my $stow = $session->stow->get("isInGroup", { noclone => 1 }) || {}; + return $stow->{$uid}->{$gid} if (exists $stow->{$uid}->{$gid}); + + ### Don't bother checking File Cache if we already have a stow for this group. + ### We can find what we need there and save ourselves a bunch of time + my $cache = undef; + my $groupMembers = undef; + unless ($stow->{$uid}->{$gid}) { + $cache = WebGUI::Cache->new($session,["groupMembers",$gid]); + $groupMembers = $cache->get || {}; + #If we have this user's membership cached, return what we have stored + if (exists $groupMembers->{$uid}) { + return $groupMembers->{$uid}->{isMember} if (!$self->isVisitor); + return $groupMembers->{$uid}->{$session->getId}->{isMember} #Include the session check for visitors + } + } + + ### Instantiate the group + my $group = WebGUI::Group->new($session,$gid); + if ( !$group ) { + #Group is not valid, check the admin group + $group = WebGUI::Group->new($session,3); + } + + #Check the group for membership + my $isInGroup = $group->hasUser($self); + + #Write what we found to file cache + $group->cacheGroupings( $self, $isInGroup, $cache, $groupMembers ); + return $isInGroup; +} #------------------------------------------------------------------- diff --git a/t/Group.t b/t/Group.t index d1cf801c4..3a7d8c026 100644 --- a/t/Group.t +++ b/t/Group.t @@ -74,8 +74,26 @@ my @ipTests = ( }, ); +my @ldapTests = ( + { + dn => 'uid=Byron Hadley,o=shawshank', + comment => 'bad dn for group', + expect => 0, + }, + { + dn => 'uid=Andy Dufresne,o=shawshank', + comment => 'good dn for group', + expect => 1, + }, + { + dn => 'uid=Bogs Diamond,o=shawshank', + comment => 'another good dn for group', + expect => 1, + }, +); -plan tests => (151 + scalar(@scratchTests) + scalar(@ipTests)); # increment this value for each test you create + +plan tests => (164 + (scalar(@scratchTests) * 2) + scalar(@ipTests)); # increment this value for each test you create my $session = WebGUI::Test->session; my $testCache = WebGUI::Cache->new($session, 'myTestKey'); @@ -167,16 +185,44 @@ $optionGroup->delete; # ################################################################ +my $ldapProps = WebGUI::Test->getSmokeLDAPProps(); +$session->db->setRow('ldapLink', 'ldapLinkId', $ldapProps, $ldapProps->{ldapLinkId}); +my $ldap = WebGUI::LDAPLink->new($session, $ldapProps->{ldapLinkId}); +is($ldap->getValue("ldapLinkId"),$ldapProps->{ldapLinkId},'ldap link created properly'); +addToCleanup($ldap); + +my @shawshank; + +foreach my $idx (0..$#ldapTests) { + $shawshank[$idx] = WebGUI::User->new($session, "new"); + $shawshank[$idx]->username("shawshank$idx"); + $shawshank[$idx]->authMethod("LDAP"); + my $auth = $shawshank[$idx]->authInstance; + $auth->saveParams($shawshank[$idx]->getId,$shawshank[$idx]->authMethod,{ + connectDN => $ldapTests[$idx]->{dn}, + ldapConnection => $ldap->getValue("ldapLinkId"), + ldapUrl => $ldap->getValue("ldapUrl"), + }); +} + +WebGUI::Test->usersToDelete(@shawshank); + my $lGroup = WebGUI::Group->new($session, 'new'); -$lGroup->ldapGroup('LDAP group'); -is($lGroup->ldapGroup(), 'LDAP group', 'ldapGroup set and fetched correctly'); +$lGroup->ldapGroup('cn=Convicts,o=shawshank'); +is($lGroup->ldapGroup(), 'cn=Convicts,o=shawshank', 'ldapGroup set and fetched correctly'); -$lGroup->ldapGroupProperty('LDAP group property'); -is($lGroup->ldapGroupProperty(), 'LDAP group property', 'ldapGroup set and fetched correctly'); +$lGroup->ldapGroupProperty('member'); +is($lGroup->ldapGroupProperty(), 'member', 'ldapGroup set and fetched correctly'); -$lGroup->ldapLinkId('LDAP link id'); -is($lGroup->ldapLinkId(), 'LDAP link id', 'ldapLinkId set and fetched correctly'); +$lGroup->ldapLinkId($ldapProps->{ldapLinkId}); +is($lGroup->ldapLinkId(),$ldapProps->{ldapLinkId}, 'ldapLinkId set and fetched correctly'); + +is_deeply( + [ (map { $lGroup->hasLDAPUser($_->getId) } @shawshank) ], + [0, 1, 1], + 'shawshank user 2, and 3 found in lGroup users from LDAP' +); $lGroup->ldapRecursiveProperty('LDAP recursive property'); is($lGroup->ldapRecursiveProperty(), 'LDAP recursive property', 'ldapRecursiveProperty set and fetched correctly'); @@ -242,6 +288,7 @@ cmp_bag($gB->getGroupsIn(), [$gA->getId, 3], 'Group A is in Group B'); cmp_bag($gA->getGroupsFor(), [$gB->getId], 'Group B contains Group A'); cmp_bag($gA->getGroupsIn(), [3], 'Admin added to group A automatically'); +diag $gA->getId; $gA->addGroups([$gB->getId]); cmp_bag($gA->getGroupsIn(), [3], 'Not allowed to create recursive group loops'); @@ -416,7 +463,7 @@ cmp_ok($expirationDate-time(), '>', 50, 'checking expire offset override on addU ################################################################ # -# getDatabaseUsers +# getDatabaseUsers & hasDatabaseUsers # ################################################################ @@ -437,7 +484,17 @@ cmp_bag($mobUsers, [map {$_->userId} @mob], 'verify SQL table built correctly'); is( $gY->databaseLinkId, 0, "Group Y's databaseLinkId is set to WebGUI"); $gY->dbQuery(q!select userId from myUserTable!); is( $session->stow->get('isInGroup'), undef, 'setting dbQuery clears cached isInGroup'); -WebGUI::Cache->new($session, $gZ->getId)->delete(); ##Delete cached key for testing + +is( $mob[0]->isInGroup($gY->getId), 1, 'mob[0] is in group Y after setting dbQuery'); +is( $mob[0]->isInGroup($gZ->getId), 1, 'mob[0] isInGroup Z'); + +ok( isIn($mob[0]->userId, @{ $gY->getAllUsers() }), 'mob[0] in list of group Y users'); +ok( !isIn($mob[0]->userId, @{ $gZ->getUsers() }), 'mob[0] not in list of group Z users'); + +ok( isIn($mob[0]->userId, @{ $gZ->getAllUsers() }), 'mob[0] in list of group Z users, recursively'); + +WebGUI::Cache->new($session, $gY->getId)->delete(); ##Delete cached key for testing +$session->stow->delete("isInGroup"); my @mobIds = map { $_->userId } @mob; @@ -447,13 +504,17 @@ cmp_bag( 'all mob users in list of group Y users from database' ); -is( $mob[0]->isInGroup($gY->getId), 1, 'mob[0] is in group Y after setting dbQuery'); -is( $mob[0]->isInGroup($gZ->getId), 1, 'mob[0] isInGroup Z'); +$session->db->write('delete from myUserTable where userId=?',[$mob[0]->getId]); +my $inDb = $session->db->quickScalar("select count(*) from myUserTable where userId=?",[$mob[0]->getId]); +ok ( !$inDb, 'mob[0] no longer in myUserTable'); +WebGUI::Cache->new($session, ["groupMembers",$gY->getId])->delete; #Delete cache so we get a good test +$session->stow->delete("isInGroup"); #Delete stow so we get a good test -ok( isIn($mob[0]->userId, @{ $gY->getAllUsers() }), 'mob[0] in list of group Y users'); -ok( !isIn($mob[0]->userId, @{ $gZ->getUsers() }), 'mob[0] not in list of group Z users'); - -ok( isIn($mob[0]->userId, @{ $gZ->getAllUsers() }), 'mob[0] in list of group Z users, recursively'); +is_deeply( + [ (map { $gY->hasDatabaseUser($_->getId) } @mob) ], + [0, 1, 1], + 'mob users 1,2 found in list of group Y users from database' +); ##Karma tests @@ -499,6 +560,12 @@ is_deeply( 'karma disabled in settings, no users in group' ); +is_deeply( + [ (map { $gK->hasKarmaUser($_->getId) } @chameleons) ], + [0, 0, 0, 0], + 'karma disabled in settings, group K has no users via karma threshold' +); + $session->setting->set('useKarma', 1); $gK->clearCaches; ##Clear cache since previous data is wrong @@ -508,6 +575,12 @@ is_deeply( 'chameleons 1, 2 and 3 are in group K via karma threshold' ); +is_deeply( + [ (map { $gK->hasKarmaUser($_->getId) } @chameleons) ], + [0, 1, 1, 1], + 'group K has chameleons 1, 2 and 3 via karma threshold' +); + cmp_bag( $gK->getKarmaUsers, [ (map { $_->userId() } @chameleons[1..3]) ], @@ -562,10 +635,20 @@ foreach my $idx (0..$#scratchTests) { WebGUI::Test->usersToDelete(@itchies); WebGUI::Test->sessionsToDelete(@sessionBank); +#isInGroup test foreach my $scratchTest (@scratchTests) { is($scratchTest->{user}->isInGroup($gS->getId), $scratchTest->{expect}, $scratchTest->{comment}); } +WebGUI::Cache->new($session, $gS->getId)->delete(); ##Delete cached key for testing +$session->stow->delete("isInGroup"); + +#hasScratchUser test +foreach my $scratchTest (@scratchTests) { + is($gS->hasScratchUser($scratchTest->{user}->getId), $scratchTest->{expect}, $scratchTest->{comment}." - hasScratchUser"); +} + + cmp_bag( $gS->getScratchUsers, [ (map { $_->{user}->userId() } grep { $_->{expect} } @scratchTests) ], @@ -578,6 +661,33 @@ cmp_bag( 'getAllUsers for group with scratch' ); +{ ##Add scope to force cleanup + + note "Checking for user Visitor session leak"; + + my $remoteSession = WebGUI::Test->newSession; + $remoteSession->user({userId => 1}); + $remoteSession->scratch->set('remote','nok'); + + my $localScratchGroup = WebGUI::Group->new($session, 'new'); + $localScratchGroup->name("Local IP Group"); + $localScratchGroup->scratchFilter('local=ok'); + + ok !$remoteSession->user->isInGroup($localScratchGroup->getId), 'Remote Visitor fails to be in the scratch group'; + + my $localSession = WebGUI::Test->newSession; + WebGUI::Test->addToCleanup($localScratchGroup, $remoteSession, $localSession); + $localSession->user({userId => 1}); + $remoteSession->scratch->set('local','ok'); + $localScratchGroup->clearCaches; + + ok $localSession->user->isInGroup($localScratchGroup->getId), 'Local Visitor is in the scratch group'; + + $remoteSession->stow->delete('isInGroup'); + ok !$remoteSession->user->isInGroup($localScratchGroup->getId), 'Remove Visitor is not in the scratch group, even though a different Visitor passed'; + +} + @sessionBank = (); my @tcps = (); @@ -619,10 +729,45 @@ cmp_bag( 'getUsers for group with IP filter' ); +is_deeply( + [ (map { $gI->hasIpUser($_->{user}->getId) } @ipTests) ], + [ (map { $_->{expect} } @ipTests) ], + 'hasIpUsers for group with IP filter' +); + foreach my $ipTest (@ipTests) { is($ipTest->{user}->isInGroup($gI->getId), $ipTest->{expect}, $ipTest->{comment}); } +{ ##Add scope to force cleanup + + note "Checking for user Visitor session leak"; + + $ENV{REMOTE_ADDR} = '191.168.1.1'; + my $remoteSession = WebGUI::Test->newSession; + $remoteSession->user({userId => 1}); + + my $localIpGroup = WebGUI::Group->new($session, 'new'); + $localIpGroup->name("Local IP Group"); + $localIpGroup->ipFilter('192.168.33.0/24'); + + ok !$remoteSession->user->isInGroup($localIpGroup->getId), 'Remote Visitor fails to be in the group'; + + $ENV{REMOTE_ADDR} = '192.168.33.1'; + my $localSession = WebGUI::Test->newSession; + WebGUI::Test->addToCleanup($localIpGroup, $remoteSession, $localSession); + $localSession->user({userId => 1}); + $localIpGroup->clearCaches; + + ok $localSession->user->isInGroup($localIpGroup->getId), 'Local Visitor is in the group'; + + $remoteSession->stow->delete('isInGroup'); + ok !$remoteSession->user->isInGroup($localIpGroup->getId), 'Remove Visitor is not in the group, even though a different Visitor passed'; + +} + + + ##Cache check. my $cacheDude = WebGUI::User->new($session, "new");