/* * Group lookup should not happen for static users */ @Test public void testGroupLookupForStaticUsers() throws Exception { conf.setClass( CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING, FakeunPrivilegedGroupMapping.class, ShellBasedUnixGroupsMapping.class); conf.set( CommonConfigurationKeys.HADOOP_USER_GROUP_STATIC_OVERRIDES, "me=;user1=group1;user2=group1,group2"); Groups groups = new Groups(conf); List<String> userGroups = groups.getGroups("me"); assertTrue("non-empty groups for static user", userGroups.isEmpty()); assertFalse("group lookup done for static user", FakeunPrivilegedGroupMapping.invoked); List<String> expected = new ArrayList<String>(); expected.add("group1"); FakeunPrivilegedGroupMapping.invoked = false; userGroups = groups.getGroups("user1"); assertTrue("groups not correct", expected.equals(userGroups)); assertFalse("group lookup done for unprivileged user", FakeunPrivilegedGroupMapping.invoked); expected.add("group2"); FakeunPrivilegedGroupMapping.invoked = false; userGroups = groups.getGroups("user2"); assertTrue("groups not correct", expected.equals(userGroups)); assertFalse("group lookup done for unprivileged user", FakeunPrivilegedGroupMapping.invoked); }
@Test public void testExceptionsFromImplNotCachedInNegativeCache() { conf.setClass( CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING, ExceptionalGroupMapping.class, ShellBasedUnixGroupsMapping.class); conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 10000); Groups groups = new Groups(conf); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); assertEquals(0, ExceptionalGroupMapping.getRequestCount()); // First call should hit the wire try { groups.getGroups("anything"); fail("Should have thrown"); } catch (IOException e) { // okay } assertEquals(1, ExceptionalGroupMapping.getRequestCount()); // Second call should hit the wire (no negative caching) try { groups.getGroups("anything"); fail("Should have thrown"); } catch (IOException e) { // okay } assertEquals(2, ExceptionalGroupMapping.getRequestCount()); }
@Override // RefreshAuthorizationPolicyProtocol public void refreshUserToGroupsMappings() throws IOException { LOG.info( "Refreshing all user-to-groups mappings. Requested by user: " + UserGroupInformation.getCurrentUser().getShortUserName()); Groups.getUserToGroupsMappingService().refresh(); }
@Override public RefreshUserToGroupsMappingsResponse refreshUserToGroupsMappings( RefreshUserToGroupsMappingsRequest request) throws YarnException, IOException { String argName = "refreshUserToGroupsMappings"; UserGroupInformation user = checkAcls(argName); if (!isRMActive()) { RMAuditLogger.logFailure( user.getShortUserName(), argName, adminAcl.toString(), "AdminService", "ResourceManager is not active. Can not refresh user-groups."); throwStandbyException(); } Groups.getUserToGroupsMappingService( getConfiguration( new Configuration(false), YarnConfiguration.CORE_SITE_CONFIGURATION_FILE)) .refresh(); RMAuditLogger.logSuccess(user.getShortUserName(), argName, "AdminService"); return recordFactory.newRecordInstance(RefreshUserToGroupsMappingsResponse.class); }
/** * Set the configuration values for UGI. * * @param conf the configuration to use */ private static synchronized void initialize(Configuration conf) { String value = conf.get(HADOOP_SECURITY_AUTHENTICATION); if (value == null || "simple".equals(value)) { useKerberos = false; useConfiguredFileAuth = false; } else if ("kerberos".equals(value)) { useKerberos = true; useConfiguredFileAuth = false; } else if ("configfile".equals(value)) { useKerberos = false; useConfiguredFileAuth = true; } else { throw new IllegalArgumentException( "Invalid attribute value for " + HADOOP_SECURITY_AUTHENTICATION + " of " + value); } // The getUserToGroupsMappingService will change the conf value, record the UGI information // firstly if (configUGIInformation == null) { configUGIInformation = conf.getStrings("hadoop.client.ugi"); } // If we haven't set up testing groups, use the configuration to find it if (!(groups instanceof TestingGroups)) { groups = Groups.getUserToGroupsMappingService(conf); } // Set the configuration for JAAS to be the Hadoop configuration. // This is done here rather than a static initializer to avoid a // circular dependence. javax.security.auth.login.Configuration existingConfig = null; try { existingConfig = javax.security.auth.login.Configuration.getConfiguration(); } catch (SecurityException se) { // If no security configuration is on the classpath, then // we catch this exception, and we don't need to delegate // to anyone } if (existingConfig instanceof HadoopConfiguration) { LOG.info("JAAS Configuration already set up for Hadoop, not re-installing."); } else { javax.security.auth.login.Configuration.setConfiguration( new HadoopConfiguration(existingConfig)); } // We're done initializing at this point. Important not to classload // KerberosName before this point, or else its static initializer // may call back into this same method! isInitialized = true; UserGroupInformation.conf = conf; // give the configuration on how to translate Kerberos names try { KerberosName.setConfiguration(conf); } catch (IOException ioe) { throw new RuntimeException( "Problem with Kerberos auth_to_local name " + "configuration", ioe); } }
@Test public void testGroupsCaching() throws Exception { // Disable negative cache. conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 0); Groups groups = new Groups(conf); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); FakeGroupMapping.clearBlackList(); FakeGroupMapping.addToBlackList("user1"); // regular entry assertTrue(groups.getGroups("me").size() == 2); // this must be cached. blacklisting should have no effect. FakeGroupMapping.addToBlackList("me"); assertTrue(groups.getGroups("me").size() == 2); // ask for a negative entry try { LOG.error("We are not supposed to get here." + groups.getGroups("user1").toString()); fail(); } catch (IOException ioe) { if (!ioe.getMessage().startsWith("No groups found")) { LOG.error("Got unexpected exception: " + ioe.getMessage()); fail(); } } // this shouldn't be cached. remove from the black list and retry. FakeGroupMapping.clearBlackList(); assertTrue(groups.getGroups("user1").size() == 2); }
@Override public Set<String> getGroups(String user) { try { return new HashSet<String>(groups.getGroups(user)); } catch (IOException e) { LOGGER.warn("Unable to obtain groups for " + user, e); } return Collections.emptySet(); }
@Test public void testOnlyOneRequestWhenExpiredEntryExists() throws Exception { conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_SECS, 1); FakeTimer timer = new FakeTimer(); final Groups groups = new Groups(conf, timer); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); FakeGroupMapping.clearBlackList(); FakeGroupMapping.setGetGroupsDelayMs(100); // We make an initial request to populate the cache groups.getGroups("me"); int startingRequestCount = FakeGroupMapping.getRequestCount(); // Then expire that entry timer.advance(400 * 1000); Thread.sleep(100); ArrayList<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threads.add( new Thread() { public void run() { try { assertEquals(2, groups.getGroups("me").size()); } catch (IOException e) { fail("Should not happen"); } } }); } // We start a bunch of threads who all see the cached value for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } // Only one extra request is made assertEquals(startingRequestCount + 1, FakeGroupMapping.getRequestCount()); }
@Test public void testNegativeCacheEntriesExpire() throws Exception { conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 2); FakeTimer timer = new FakeTimer(); // Ensure that stale entries are removed from negative cache every 2 seconds Groups groups = new Groups(conf, timer); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); // Add both these users to blacklist so that they // can be added to negative cache FakeGroupMapping.addToBlackList("user1"); FakeGroupMapping.addToBlackList("user2"); // Put user1 in negative cache. try { groups.getGroups("user1"); fail("Did not throw IOException : Failed to obtain groups" + " from FakeGroupMapping."); } catch (IOException e) { GenericTestUtils.assertExceptionContains("No groups found for user", e); } // Check if user1 exists in negative cache assertTrue(groups.getNegativeCache().contains("user1")); // Advance fake timer timer.advance(1000); // Put user2 in negative cache try { groups.getGroups("user2"); fail("Did not throw IOException : Failed to obtain groups" + " from FakeGroupMapping."); } catch (IOException e) { GenericTestUtils.assertExceptionContains("No groups found for user", e); } // Check if user2 exists in negative cache assertTrue(groups.getNegativeCache().contains("user2")); // Advance timer. Only user2 should be present in negative cache. timer.advance(1100); assertFalse(groups.getNegativeCache().contains("user1")); assertTrue(groups.getNegativeCache().contains("user2")); // Advance timer. Even user2 should not be present in negative cache. timer.advance(1000); assertFalse(groups.getNegativeCache().contains("user2")); }
@Test public void testCacheEntriesExpire() throws Exception { conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_CACHE_SECS, 1); FakeTimer timer = new FakeTimer(); final Groups groups = new Groups(conf, timer); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); FakeGroupMapping.clearBlackList(); // We make an entry groups.getGroups("me"); int startingRequestCount = FakeGroupMapping.getRequestCount(); timer.advance(20 * 1000); // Cache entry has expired so it results in a new fetch groups.getGroups("me"); assertEquals(startingRequestCount + 1, FakeGroupMapping.getRequestCount()); }
@Test public void testCachePreventsImplRequest() throws Exception { // Disable negative cache. conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 0); Groups groups = new Groups(conf); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); FakeGroupMapping.clearBlackList(); assertEquals(0, FakeGroupMapping.getRequestCount()); // First call hits the wire assertTrue(groups.getGroups("me").size() == 2); assertEquals(1, FakeGroupMapping.getRequestCount()); // Second count hits cache assertTrue(groups.getGroups("me").size() == 2); assertEquals(1, FakeGroupMapping.getRequestCount()); }
/** * Get the group names for this user. * * @return the list of users with the primary group first. If the command fails, it returns an * empty list. */ public synchronized String[] getGroupNames() { ensureInitialized(); try { List<String> result = groups.getGroups(getShortUserName()); return result.toArray(new String[result.size()]); } catch (IOException ie) { LOG.warn("No groups available for user " + getShortUserName()); return new String[0]; } }
public static void refreshGroupPassword() { if (groups != null) { GroupMappingServiceProvider gmspImpl = groups.getUserToGroupsMappingServiceImpl(); if (gmspImpl != null) { try { gmspImpl.cacheGroupsRefresh(); } catch (Exception e) { LOG.warn("UIG refreshGroupPassword fail: " + e); } } } }
@Test public void testNegativeCacheClearedOnRefresh() throws Exception { conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 100); final Groups groups = new Groups(conf); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); FakeGroupMapping.clearBlackList(); FakeGroupMapping.addToBlackList("dne"); try { groups.getGroups("dne"); fail("Should have failed to find this group"); } catch (IOException e) { // pass } int startingRequestCount = FakeGroupMapping.getRequestCount(); groups.refresh(); FakeGroupMapping.addToBlackList("dne"); try { List<String> g = groups.getGroups("dne"); fail("Should have failed to find this group"); } catch (IOException e) { // pass } assertEquals(startingRequestCount + 1, FakeGroupMapping.getRequestCount()); }
@Before public void setUp() throws Exception { config = new Configuration(); config.setClass( "hadoop.security.group.mapping", TestRefreshUserMappings.MockUnixGroupsMapping.class, GroupMappingServiceProvider.class); config.setLong("hadoop.security.groups.cache.secs", groupRefreshTimeoutSec); Groups.getUserToGroupsMappingService(config); FileSystem.setDefaultUri(config, "hdfs://localhost:" + "0"); cluster = new MiniDFSCluster(0, config, 1, true, true, true, null, null, null, null); cluster.waitActive(); }
@Test public void testOnlyOneRequestWhenNoEntryIsCached() throws Exception { // Disable negative cache. conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 0); final Groups groups = new Groups(conf); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); FakeGroupMapping.clearBlackList(); FakeGroupMapping.setGetGroupsDelayMs(100); ArrayList<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threads.add( new Thread() { public void run() { try { assertEquals(2, groups.getGroups("me").size()); } catch (IOException e) { fail("Should not happen"); } } }); } // We start a bunch of threads who all see no cached value for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } // But only one thread should have made the request assertEquals(1, FakeGroupMapping.getRequestCount()); }
@Test public void testNegativeGroupCaching() throws Exception { final String user = "******"; final String failMessage = "Did not throw IOException: "; conf.setLong(CommonConfigurationKeys.HADOOP_SECURITY_GROUPS_NEGATIVE_CACHE_SECS, 2); FakeTimer timer = new FakeTimer(); Groups groups = new Groups(conf, timer); groups.cacheGroupsAdd(Arrays.asList(myGroups)); groups.refresh(); FakeGroupMapping.addToBlackList(user); // In the first attempt, the user will be put in the negative cache. try { groups.getGroups(user); fail(failMessage + "Failed to obtain groups from FakeGroupMapping."); } catch (IOException e) { // Expects to raise exception for the first time. But the user will be // put into the negative cache GenericTestUtils.assertExceptionContains("No groups found for user", e); } // The second time, the user is in the negative cache. try { groups.getGroups(user); fail(failMessage + "The user is in the negative cache."); } catch (IOException e) { GenericTestUtils.assertExceptionContains("No groups found for user", e); } // Brings back the backend user-group mapping service. FakeGroupMapping.clearBlackList(); // It should still get groups from the negative cache. try { groups.getGroups(user); fail( failMessage + "The user is still in the negative cache, even " + "FakeGroupMapping has resumed."); } catch (IOException e) { GenericTestUtils.assertExceptionContains("No groups found for user", e); } // Let the elements in the negative cache expire. timer.advance(4 * 1000); // The groups for the user is expired in the negative cache, a new copy of // groups for the user is fetched. assertEquals(Arrays.asList(myGroups), groups.getGroups(user)); }
/** whether user is authorized? */ public boolean isAuthorized() { if (!useConfiguredFileAuth) { return true; } Set privateCredList = subject.getPrivateCredentials(); String userName = user.getName(); String userPassword = null; if ((userName == null) || (privateCredList == null)) { return false; } userPassword = getPassword(); try { String correctPassword = groups.getPassword(user.getName()); if ((correctPassword != null) && (correctPassword.equals(userPassword))) { return true; } } catch (Exception e) { e.printStackTrace(); } return false; }
@Test public void testGroupMappingRefresh() throws Exception { DFSAdmin admin = new DFSAdmin(config); String[] args = new String[] {"-refreshUserToGroupsMappings"}; Groups groups = Groups.getUserToGroupsMappingService(config); String user = UserGroupInformation.getCurrentUser().getUserName(); System.out.println("first attempt:"); List<String> g1 = groups.getGroups(user); String[] str_groups = new String[g1.size()]; g1.toArray(str_groups); System.out.println(Arrays.toString(str_groups)); System.out.println("second attempt, should be same:"); List<String> g2 = groups.getGroups(user); g2.toArray(str_groups); System.out.println(Arrays.toString(str_groups)); for (int i = 0; i < g2.size(); i++) { assertEquals("Should be same group ", g1.get(i), g2.get(i)); } admin.run(args); System.out.println("third attempt(after refresh command), should be different:"); List<String> g3 = groups.getGroups(user); g3.toArray(str_groups); System.out.println(Arrays.toString(str_groups)); for (int i = 0; i < g3.size(); i++) { assertFalse( "Should be different group: " + g1.get(i) + " and " + g3.get(i), g1.get(i).equals(g3.get(i))); } // test time out Thread.sleep(groupRefreshTimeoutSec * 1100); System.out.println("fourth attempt(after timeout), should be different:"); List<String> g4 = groups.getGroups(user); g4.toArray(str_groups); System.out.println(Arrays.toString(str_groups)); for (int i = 0; i < g4.size(); i++) { assertFalse("Should be different group ", g3.get(i).equals(g4.get(i))); } }
@Test public void testRMInitialsWithFileSystemBasedConfigurationProvider() throws Exception { configuration.set( YarnConfiguration.RM_CONFIGURATION_PROVIDER_CLASS, "org.apache.hadoop.yarn.FileSystemBasedConfigurationProvider"); // upload configurations final File excludeHostsFile = new File(tmpDir.toString(), "excludeHosts"); if (excludeHostsFile.exists()) { excludeHostsFile.delete(); } if (!excludeHostsFile.createNewFile()) { Assert.fail("Can not create " + "excludeHosts"); } PrintWriter fileWriter = new PrintWriter(excludeHostsFile); fileWriter.write("0.0.0.0:123"); fileWriter.close(); uploadToRemoteFileSystem(new Path(excludeHostsFile.getAbsolutePath())); YarnConfiguration yarnConf = new YarnConfiguration(); yarnConf.set(YarnConfiguration.YARN_ADMIN_ACL, "world:anyone:rwcda"); yarnConf.set(YarnConfiguration.RM_NODES_EXCLUDE_FILE_PATH, this.workingPath + "/excludeHosts"); uploadConfiguration(yarnConf, "yarn-site.xml"); CapacitySchedulerConfiguration csConf = new CapacitySchedulerConfiguration(); csConf.set("yarn.scheduler.capacity.maximum-applications", "5000"); uploadConfiguration(csConf, "capacity-scheduler.xml"); String aclsString = "alice,bob users,wheel"; Configuration newConf = new Configuration(); newConf.set("security.applicationclient.protocol.acl", aclsString); uploadConfiguration(newConf, "hadoop-policy.xml"); Configuration conf = new Configuration(); conf.setBoolean(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHORIZATION, true); conf.set("hadoop.proxyuser.test.groups", "test_groups"); conf.set("hadoop.proxyuser.test.hosts", "test_hosts"); conf.setClass( CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING, MockUnixGroupsMapping.class, GroupMappingServiceProvider.class); uploadConfiguration(conf, "core-site.xml"); // update the groups MockUnixGroupsMapping.updateGroups(); ResourceManager resourceManager = null; try { try { resourceManager = new ResourceManager(); resourceManager.init(configuration); resourceManager.start(); } catch (Exception ex) { fail("Should not get any exceptions"); } // validate values for excludeHosts Set<String> excludeHosts = resourceManager.getRMContext().getNodesListManager().getHostsReader().getExcludedHosts(); Assert.assertTrue(excludeHosts.size() == 1); Assert.assertTrue(excludeHosts.contains("0.0.0.0:123")); // validate values for admin-acls String aclStringAfter = resourceManager.adminService.getAccessControlList().getAclString().trim(); Assert.assertEquals(aclStringAfter, "world:anyone:rwcda"); // validate values for queue configuration CapacityScheduler cs = (CapacityScheduler) resourceManager.getRMContext().getScheduler(); int maxAppsAfter = cs.getConfiguration().getMaximumSystemApplications(); Assert.assertEquals(maxAppsAfter, 5000); // verify service Acls for AdminService ServiceAuthorizationManager adminServiceServiceManager = resourceManager.adminService.getServer().getServiceAuthorizationManager(); verifyServiceACLsRefresh( adminServiceServiceManager, org.apache.hadoop.yarn.api.ApplicationClientProtocolPB.class, aclsString); // verify service ACLs for ClientRMService ServiceAuthorizationManager clientRMServiceServiceManager = resourceManager .getRMContext() .getClientRMService() .getServer() .getServiceAuthorizationManager(); verifyServiceACLsRefresh( clientRMServiceServiceManager, org.apache.hadoop.yarn.api.ApplicationClientProtocolPB.class, aclsString); // verify service ACLs for ApplicationMasterService ServiceAuthorizationManager appMasterService = resourceManager .getRMContext() .getApplicationMasterService() .getServer() .getServiceAuthorizationManager(); verifyServiceACLsRefresh( appMasterService, org.apache.hadoop.yarn.api.ApplicationClientProtocolPB.class, aclsString); // verify service ACLs for ResourceTrackerService ServiceAuthorizationManager RTService = resourceManager .getRMContext() .getResourceTrackerService() .getServer() .getServiceAuthorizationManager(); verifyServiceACLsRefresh( RTService, org.apache.hadoop.yarn.api.ApplicationClientProtocolPB.class, aclsString); // verify ProxyUsers and ProxyHosts Assert.assertTrue( ProxyUsers.getProxyGroups().get("hadoop.proxyuser.test.groups").size() == 1); Assert.assertTrue( ProxyUsers.getProxyGroups().get("hadoop.proxyuser.test.groups").contains("test_groups")); Assert.assertTrue(ProxyUsers.getProxyHosts().get("hadoop.proxyuser.test.hosts").size() == 1); Assert.assertTrue( ProxyUsers.getProxyHosts().get("hadoop.proxyuser.test.hosts").contains("test_hosts")); // verify UserToGroupsMappings List<String> groupAfter = Groups.getUserToGroupsMappingService(configuration) .getGroups(UserGroupInformation.getCurrentUser().getUserName()); Assert.assertTrue( groupAfter.contains("test_group_D") && groupAfter.contains("test_group_E") && groupAfter.contains("test_group_F") && groupAfter.size() == 3); } finally { if (resourceManager != null) { resourceManager.stop(); } } }
@Test public void testRefreshUserToGroupsMappingsWithFileSystemBasedConfigurationProvider() throws IOException, YarnException { configuration.set( YarnConfiguration.RM_CONFIGURATION_PROVIDER_CLASS, "org.apache.hadoop.yarn.FileSystemBasedConfigurationProvider"); String[] defaultTestUserGroups = {"dummy_group1", "dummy_group2"}; UserGroupInformation ugi = UserGroupInformation.createUserForTesting("dummyUser", defaultTestUserGroups); String user = ugi.getUserName(); List<String> groupWithInit = new ArrayList<String>(2); for (int i = 0; i < ugi.getGroupNames().length; i++) { groupWithInit.add(ugi.getGroupNames()[i]); } // upload default configurations uploadDefaultConfiguration(); Configuration conf = new Configuration(); conf.setClass( CommonConfigurationKeys.HADOOP_SECURITY_GROUP_MAPPING, MockUnixGroupsMapping.class, GroupMappingServiceProvider.class); uploadConfiguration(conf, "core-site.xml"); try { rm = new MockRM(configuration); rm.init(configuration); rm.start(); } catch (Exception ex) { fail("Should not get any exceptions"); } // Make sure RM will use the updated GroupMappingServiceProvider List<String> groupBefore = new ArrayList<String>(Groups.getUserToGroupsMappingService(configuration).getGroups(user)); Assert.assertTrue( groupBefore.contains("test_group_A") && groupBefore.contains("test_group_B") && groupBefore.contains("test_group_C") && groupBefore.size() == 3); Assert.assertTrue(groupWithInit.size() != groupBefore.size()); Assert.assertFalse( groupWithInit.contains("test_group_A") || groupWithInit.contains("test_group_B") || groupWithInit.contains("test_group_C")); // update the groups MockUnixGroupsMapping.updateGroups(); rm.adminService.refreshUserToGroupsMappings(RefreshUserToGroupsMappingsRequest.newInstance()); List<String> groupAfter = Groups.getUserToGroupsMappingService(configuration).getGroups(user); // should get the updated groups Assert.assertTrue( groupAfter.contains("test_group_D") && groupAfter.contains("test_group_E") && groupAfter.contains("test_group_F") && groupAfter.size() == 3); }