diff --git a/oak-core/pom.xml b/oak-core/pom.xml index 05b1eee..827e197 100644 --- a/oak-core/pom.xml +++ b/oak-core/pom.xml @@ -225,5 +225,11 @@ 1.0.1 test + + org.apache.directory.server + apacheds-server-unit + 1.5.5 + test + diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/JndiLdapSearch.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/JndiLdapSearch.java index a5a7c49..d34ca59 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/JndiLdapSearch.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/JndiLdapSearch.java @@ -23,6 +23,7 @@ import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; @@ -89,11 +90,16 @@ public class JndiLdapSearch implements LdapSearch { Map properties = new HashMap(); Map syncMap = user instanceof LdapGroup ? settings.getGroupAttributes() : settings.getUserAttributes(); + Map lcSyncMap = new HashMap(); + for (Map.Entry entry : syncMap.entrySet()) { + String key = entry.getKey(); + lcSyncMap.put(key == null? null : key.toLowerCase(), entry.getValue()); + } while (namingEnumeration.hasMore()) { Attribute attribute = namingEnumeration.next(); - String key = attribute.getID(); - if (syncMap.containsKey(key)) { - properties.put(syncMap.get(key), parseAttributeValue(attribute)); + String key = attribute.getID().toLowerCase(); + if (lcSyncMap.containsKey(key)) { + properties.put(lcSyncMap.get(key), parseAttributeValue(attribute)); } } user.setProperties(properties); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/DefaultSyncHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/DefaultSyncHandler.java index 3f265b3..28722c3 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/DefaultSyncHandler.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/DefaultSyncHandler.java @@ -111,7 +111,7 @@ public class DefaultSyncHandler implements SyncHandler { @CheckForNull private User createUser(ExternalUser externalUser) throws RepositoryException, SyncException { if (mode.contains(SyncMode.MODE_CREATE_USER)) { - User user = userManager.createUser(externalUser.getId(), externalUser.getPassword(), externalUser.getPrincipal(), externalUser.getPath()); + User user = userManager.createUser(externalUser.getId(), externalUser.getPassword(), externalUser.getPrincipal(), null); syncAuthorizable(externalUser, user); return user; } else { @@ -122,7 +122,7 @@ public class DefaultSyncHandler implements SyncHandler { @CheckForNull private Group createGroup(ExternalGroup externalGroup) throws RepositoryException, SyncException { if (mode.contains(SyncMode.MODE_CREATE_GROUPS)) { - Group group = userManager.createGroup(externalGroup.getId(), externalGroup.getPrincipal(), externalGroup.getPath()); + Group group = userManager.createGroup(externalGroup.getId(), externalGroup.getPrincipal(), null); syncAuthorizable(externalGroup, group); return group; } else { diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/InternalLdapServer.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/InternalLdapServer.java new file mode 100644 index 0000000..ef2cd9b --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/InternalLdapServer.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.ldap; + +import java.io.File; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.DirContext; +import javax.naming.ldap.LdapContext; + +import org.apache.directory.server.constants.ServerDNConstants; +import org.apache.directory.server.unit.AbstractServerTest; + +class InternalLdapServer extends AbstractServerTest { + + public static final String GROUP_MEMBER_ATTR = "member"; + public static final String GROUP_CLASS_ATTR = "groupOfNames"; + + public static final String ADMIN_PW = "secret"; + + public void setUp() throws Exception { + super.setUp(); + doDelete = true; + } + + public void tearDown() throws Exception { + super.tearDown(); + } + + @Override + protected void configureDirectoryService() throws Exception { + directoryService.setWorkingDirectory(new File("target", "apacheds")); + doDelete(directoryService.getWorkingDirectory()); + } + + public int getPort() { + return port; + } + + public String addUser(String firstName, String lastName, String userId, String password) + throws Exception { + String cn = firstName + " " + lastName; + String dn = "cn=" + cn + "," + ServerDNConstants.USERS_SYSTEM_DN; + String entries = "dn: " + dn + "\n" + + "objectClass: inetOrgPerson\n" + + "cn: " + cn + "\n" + + "sn: " + lastName + "\n" + + "givenName:" + firstName + "\n" + + "uid: " + userId + "\n" + + "userPassword: " + password + "\n\n"; + injectEntries(entries); + return dn; + } + + public String addGroup(String name) throws Exception { + String dn = "cn=" + name + "," + ServerDNConstants.GROUPS_SYSTEM_DN; + String entries = "dn: " + dn + "\n" + + "objectClass: " + GROUP_CLASS_ATTR + "\n" + + GROUP_MEMBER_ATTR + ":\n" + + "cn: " + name + "\n\n"; + injectEntries(entries); + return dn; + } + + public void addMember(String groupDN, String memberDN) throws Exception { + LdapContext ctxt = getWiredContext(); + BasicAttributes attrs = new BasicAttributes(); + attrs.put("member", memberDN); + ctxt.modifyAttributes(groupDN, DirContext.ADD_ATTRIBUTE, attrs); + } + + public void removeMember(String groupDN, String memberDN) throws Exception { + LdapContext ctxt = getWiredContext(); + BasicAttributes attrs = new BasicAttributes(); + attrs.put("member", memberDN); + ctxt.modifyAttributes(groupDN, DirContext.REMOVE_ATTRIBUTE, attrs); + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginStandaloneTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginStandaloneTest.java new file mode 100644 index 0000000..948c496 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginStandaloneTest.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.ldap; + +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalLoginModule; +import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncMode; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@Ignore //ignore for the moment because "mvn test" runs into PermGen memory issues +public class LdapLoginStandaloneTest extends LdapLoginTestBase { + + @Override + protected Configuration getConfiguration() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String s) { + return new AppConfigurationEntry[]{ + new AppConfigurationEntry( + LdapLoginModule.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options) + }; + } + }; + } + @Test + public void testSyncUpdateAndGroups() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + createLdapFixture(); + } + + options.put(ExternalLoginModule.PARAM_SYNC_MODE, new String[]{SyncMode.UPDATE, SyncMode.CREATE_GROUP}); + + // create user upfront in order to test update mode + userManager.createUser(USER_ID, USER_PWD); + root.commit(); + + ContentSession cs = null; + try { + cs = login(new SimpleCredentials(USER_ID, USER_PWD.toCharArray())); + + root.refresh(); + Authorizable user = userManager.getAuthorizable(USER_ID); + assertNotNull(user); + assertTrue(user.hasProperty(USER_PROP)); + Authorizable group = userManager.getAuthorizable(GROUP_DN); + assertTrue(group.hasProperty(GROUP_PROP)); + assertNotNull(group); + } finally { + if (cs != null) { + cs.close(); + } + options.clear(); + } + } + + @Test + public void testDefaultSync() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + createLdapFixture(); + } + + options.put(ExternalLoginModule.PARAM_SYNC_MODE, null); + + // create user upfront in order to test update mode + userManager.createUser(USER_ID, USER_PWD); + root.commit(); + + ContentSession cs = null; + try { + cs = login(new SimpleCredentials(USER_ID, USER_PWD.toCharArray())); + + root.refresh(); + Authorizable user = userManager.getAuthorizable(USER_ID); + assertNotNull(user); + assertTrue(user.hasProperty(USER_PROP)); + Authorizable group = userManager.getAuthorizable(GROUP_DN); + assertTrue(group.hasProperty(GROUP_PROP)); + assertNotNull(group); + } finally { + if (cs != null) { + cs.close(); + } + options.clear(); + } + } + + @Test + public void testSyncUpdate() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + createLdapFixture(); + } + + options.put(ExternalLoginModule.PARAM_SYNC_MODE, SyncMode.UPDATE); + + // create user upfront in order to test update mode + userManager.createUser(USER_ID, USER_PWD); + root.commit(); + + ContentSession cs = null; + try { + cs = login(new SimpleCredentials(USER_ID, USER_PWD.toCharArray())); + + root.refresh(); + Authorizable user = userManager.getAuthorizable(USER_ID); + assertNotNull(user); + assertTrue(user.hasProperty(USER_PROP)); + assertNull(userManager.getAuthorizable(GROUP_DN)); + } finally { + if (cs != null) { + cs.close(); + } + options.clear(); + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginTestBase.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginTestBase.java new file mode 100644 index 0000000..9fa8144 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginTestBase.java @@ -0,0 +1,278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.ldap; + +import java.util.HashMap; +import javax.jcr.SimpleCredentials; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginException; + +import org.apache.directory.server.constants.ServerDNConstants; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.AbstractSecurityTest; +import org.apache.jackrabbit.oak.api.ContentSession; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.namepath.NamePathMapper; +import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalLoginModule; +import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncMode; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public abstract class LdapLoginTestBase extends AbstractSecurityTest { + + protected static final InternalLdapServer LDAP_SERVER = new InternalLdapServer(); + + protected static final String USER_ID = "foobar"; + protected static final String USER_PWD = "foobar"; + protected static final String USER_FIRSTNAME = "Foo"; + protected static final String USER_LASTNAME = "Bar"; + protected static final String USER_ATTR = "givenName"; + protected static final String USER_PROP = "profile/name"; + protected static final String GROUP_PROP = "profile/member"; + protected static final String GROUP_NAME = "foobargroup"; + + protected static String GROUP_DN; + + //initialize LDAP server only once (fast, but might turn out to be not sufficiently flexible in the future) + protected static final boolean USE_COMMON_LDAP_FIXTURE = true; + + protected final HashMap options = new HashMap(); + + protected UserManager userManager; + + protected Root root; + + @BeforeClass + public static void beforeClass() throws Exception { + if (USE_COMMON_LDAP_FIXTURE) { + LDAP_SERVER.setUp(); + createLdapFixture(); + } + } + + @AfterClass + public static void afterClass() throws Exception { + if (USE_COMMON_LDAP_FIXTURE) { + LDAP_SERVER.tearDown(); + } + } + + @Override + protected Configuration getConfiguration() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String s) { + return new AppConfigurationEntry[]{ +// new AppConfigurationEntry( +// LoginModuleImpl.class.getName(), +// AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, +// Collections.emptyMap()), + new AppConfigurationEntry( + LdapLoginModule.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options) + }; + } + }; + } + + @Before + public void before() throws Exception { + super.before(); + + if (!USE_COMMON_LDAP_FIXTURE) { + LDAP_SERVER.setUp(); + } + + options.put(LdapSettings.KEY_HOST, "127.0.0.1"); + options.put(LdapSettings.KEY_PORT, "" + LDAP_SERVER.getPort()); + options.put(LdapSettings.KEY_AUTHDN, ServerDNConstants.ADMIN_SYSTEM_DN); + options.put(LdapSettings.KEY_AUTHPW, InternalLdapServer.ADMIN_PW); + options.put(LdapSettings.KEY_USERROOT, ServerDNConstants.USERS_SYSTEM_DN); + options.put(LdapSettings.KEY_GROUPROOT, ServerDNConstants.GROUPS_SYSTEM_DN); + options.put(LdapSettings.KEY_AUTOCREATEUSER + USER_ATTR, USER_PROP); + options.put(LdapSettings.KEY_AUTOCREATEGROUP + InternalLdapServer.GROUP_MEMBER_ATTR, GROUP_PROP); + options.put(LdapSettings.KEY_GROUPFILTER, "(objectclass=" + InternalLdapServer.GROUP_CLASS_ATTR + ")"); + options.put(LdapSettings.KEY_GROUPMEMBERSHIPATTRIBUTE, InternalLdapServer.GROUP_MEMBER_ATTR); + options.put(ExternalLoginModule.PARAM_SYNC_MODE, SyncMode.CREATE_USER); + + root = adminSession.getLatestRoot(); + userManager = securityProvider.getUserConfiguration().getUserManager(root, NamePathMapper.DEFAULT); + } + + @After + public void after() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + LDAP_SERVER.tearDown(); + } + + try { + Authorizable a = userManager.getAuthorizable(USER_ID); + if (a != null) { + a.remove(); + } + if (GROUP_DN != null) { + a = userManager.getAuthorizable(GROUP_DN); + if (a != null) { + a.remove(); + } + } + root.commit(); + } finally { + root.refresh(); + super.after(); + } + } + + @Test + public void testLoginFailed() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + createLdapFixture(); + } + + try { + ContentSession cs = login(new SimpleCredentials(USER_ID, new char[0])); + cs.close(); + fail("login failure expected"); + } catch (LoginException e) { + // success + } finally { + assertNull(userManager.getAuthorizable(USER_ID)); + } + } + + @Test + public void testSyncCreateUser() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + createLdapFixture(); + } + + options.put(ExternalLoginModule.PARAM_SYNC_MODE, SyncMode.CREATE_USER); + + ContentSession cs = null; + try { + cs = login(new SimpleCredentials(USER_ID, USER_PWD.toCharArray())); + + root.refresh(); + Authorizable user = userManager.getAuthorizable(USER_ID); + assertNotNull(user); + assertTrue(user.hasProperty(USER_PROP)); + assertNull(userManager.getAuthorizable(GROUP_DN)); + } finally { + if (cs != null) { + cs.close(); + } + options.clear(); + } + } + + @Test + public void testSyncCreateGroup() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + createLdapFixture(); + } + + options.put(ExternalLoginModule.PARAM_SYNC_MODE, SyncMode.CREATE_GROUP); + + ContentSession cs = null; + try { + cs = login(new SimpleCredentials(USER_ID, USER_PWD.toCharArray())); + + root.refresh(); + assertNull(userManager.getAuthorizable(USER_ID)); + assertNull(userManager.getAuthorizable(GROUP_DN)); + } finally { + if (cs != null) { + cs.close(); + } + options.clear(); + } + } + + @Test + public void testSyncCreateUserAndGroups() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + createLdapFixture(); + } + + options.put(ExternalLoginModule.PARAM_SYNC_MODE, new String[]{SyncMode.CREATE_USER, SyncMode.CREATE_GROUP}); + + ContentSession cs = null; + try { + cs = login(new SimpleCredentials(USER_ID, USER_PWD.toCharArray())); + + root.refresh(); + Authorizable user = userManager.getAuthorizable(USER_ID); + assertNotNull(user); + assertTrue(user.hasProperty(USER_PROP)); + Authorizable group = userManager.getAuthorizable(GROUP_DN); + assertTrue(group.hasProperty(GROUP_PROP)); + assertNotNull(group); + } finally { + if (cs != null) { + cs.close(); + } + options.clear(); + } + } + + @Test + public void testNoSync() throws Exception { + + if (!USE_COMMON_LDAP_FIXTURE) { + createLdapFixture(); + } + + options.put(ExternalLoginModule.PARAM_SYNC_MODE, ""); + + ContentSession cs = null; + try { + cs = login(new SimpleCredentials(USER_ID, USER_PWD.toCharArray())); + + root.refresh(); + assertNull(userManager.getAuthorizable(USER_ID)); + assertNull(userManager.getAuthorizable(GROUP_DN)); + } finally { + if (cs != null) { + cs.close(); + } + options.clear(); + } + } + + protected static void createLdapFixture() throws Exception { + + LDAP_SERVER.addMember( + GROUP_DN = LDAP_SERVER.addGroup(GROUP_NAME), + LDAP_SERVER.addUser(USER_FIRSTNAME, USER_LASTNAME, USER_ID, USER_PWD)); + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginWithRepoLoginTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginWithRepoLoginTest.java new file mode 100644 index 0000000..7ef8c7c --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginWithRepoLoginTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.ldap; + +import java.util.Collections; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +import org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl; +import org.junit.Ignore; + +@Ignore //ignore for the moment because "mvn test" runs into PermGen memory issues +public class LdapLoginWithRepoLoginTest extends LdapLoginTestBase { + + @Override + protected Configuration getConfiguration() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String s) { + return new AppConfigurationEntry[]{ + new AppConfigurationEntry( + LoginModuleImpl.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, + Collections.emptyMap()), + new AppConfigurationEntry( + LdapLoginModule.class.getName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options) + }; + } + }; + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTest.java index c17dcf1..355212c 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTest.java @@ -42,7 +42,7 @@ import static org.junit.Assert.fail; */ public class ExternalLoginModuleTest extends AbstractSecurityTest { - private final HashMap options = new HashMap(); + protected final HashMap options = new HashMap(); private String userId; private Set ids = new HashSet();