/* * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed 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. * * Contributors: * George Lefter */ package org.nuxeo.ecm.directory.sql; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.Serializable; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import org.apache.commons.lang.StringUtils; import org.junit.Ignore; import org.junit.Test; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.test.annotations.Granularity; import org.nuxeo.ecm.core.test.annotations.RepositoryConfig; import org.nuxeo.ecm.directory.AbstractDirectory; import org.nuxeo.ecm.directory.BaseSession; import org.nuxeo.ecm.directory.DirectoryException; import org.nuxeo.ecm.directory.PasswordHelper; import org.nuxeo.ecm.directory.Reference; import org.nuxeo.ecm.directory.Session; import org.nuxeo.ecm.directory.api.DirectoryService; import org.nuxeo.runtime.test.runner.LocalDeploy; @RepositoryConfig(cleanup = Granularity.METHOD) @LocalDeploy({ "org.nuxeo.ecm.directory.sql.tests:test-sql-directories-schema-override.xml", "org.nuxeo.ecm.directory.sql.tests:test-sql-directories-bundle.xml" }) public abstract class SQLDirectoryTestSuite { protected static final String USER_DIR = "userDirectory"; protected static final String GROUP_DIR = "groupDirectory"; protected static final String SCHEMA = "user"; @Inject protected DirectoryService directoryService; public static Calendar getCalendar(int year, int month, int day, int hours, int minutes, int seconds, int milliseconds) { Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, month - 1); // 0-based cal.set(Calendar.DAY_OF_MONTH, day); cal.set(Calendar.HOUR_OF_DAY, hours); cal.set(Calendar.MINUTE, minutes); cal.set(Calendar.SECOND, seconds); cal.set(Calendar.MILLISECOND, milliseconds); return cal; } public static Calendar stripMillis(Calendar calendar) { return getCalendar(calendar.get(Calendar.YEAR), // calendar.get(Calendar.MONTH) + 1, // calendar.get(Calendar.DAY_OF_MONTH), // calendar.get(Calendar.HOUR_OF_DAY), // calendar.get(Calendar.MINUTE), // calendar.get(Calendar.SECOND), 0); } public static void assertCalendarEquals(Calendar expected, Calendar actual) throws Exception { if (expected.equals(actual)) { return; } // try without milliseconds for stupid MySQL if (stripMillis(expected).equals(stripMillis(actual))) { return; } assertEquals(expected, actual); // proper failure } public Session getSession() throws Exception { return directoryService.open(USER_DIR); } public SQLDirectory getSQLDirectory() throws Exception { return (SQLDirectory) directoryService.getDirectory(USER_DIR); } @Test public void testTableReference() throws Exception { Reference membersRef = directoryService.getDirectory(GROUP_DIR).getReference("members"); // test initial configuration List<String> administrators = membersRef.getTargetIdsForSource("administrators"); assertEquals(1, administrators.size()); assertTrue(administrators.contains("Administrator")); // add user_1 to the administrators group membersRef.addLinks("administrators", Arrays.asList("user_1")); administrators = membersRef.getTargetIdsForSource("administrators"); assertEquals(2, administrators.size()); assertTrue(administrators.contains("Administrator")); assertTrue(administrators.contains("user_1")); // readding the same link should not duplicate it membersRef.addLinks("administrators", Arrays.asList("user_1", "user_2")); administrators = membersRef.getTargetIdsForSource("administrators"); assertEquals(3, administrators.size()); assertTrue(administrators.contains("Administrator")); assertTrue(administrators.contains("user_1")); assertTrue(administrators.contains("user_2")); // remove the reference to Administrator membersRef.removeLinksForTarget("Administrator"); administrators = membersRef.getTargetIdsForSource("administrators"); assertEquals(2, administrators.size()); assertTrue(administrators.contains("user_1")); assertTrue(administrators.contains("user_2")); // remove the references from administrators membersRef.removeLinksForSource("administrators"); administrators = membersRef.getTargetIdsForSource("administrators"); assertEquals(0, administrators.size()); // readd references with the set* methods membersRef.setTargetIdsForSource("administrators", Arrays.asList("user_1", "user_2")); administrators = membersRef.getTargetIdsForSource("administrators"); assertEquals(2, administrators.size()); assertTrue(administrators.contains("user_1")); assertTrue(administrators.contains("user_2")); membersRef.setTargetIdsForSource("administrators", Arrays.asList("user_1", "Administrator")); administrators = membersRef.getTargetIdsForSource("administrators"); assertEquals(2, administrators.size()); assertTrue(administrators.contains("user_1")); assertTrue(administrators.contains("Administrator")); membersRef.setSourceIdsForTarget("Administrator", Arrays.asList("members")); administrators = membersRef.getTargetIdsForSource("administrators"); assertEquals(1, administrators.size()); assertTrue(administrators.contains("user_1")); administrators = membersRef.getSourceIdsForTarget("Administrator"); assertEquals(1, administrators.size()); assertTrue(administrators.contains("members")); // cleanup for other tests // (because the feature doesn't clean up everything) membersRef.setTargetIdsForSource("administrators", Arrays.asList("Administrator")); membersRef.setTargetIdsForSource("members", Arrays.asList("user_1")); } @SuppressWarnings("unchecked") @Test public void testCreateEntry() throws Exception { try (Session session = getSession()) { Map<String, Object> map = new HashMap<String, Object>(); map.put("username", "user_0"); map.put("password", "pass_0"); map.put("intField", Long.valueOf(5)); map.put("dateField", getCalendar(1982, 3, 25, 16, 30, 47, 0)); map.put("groups", Arrays.asList("members", "administrators")); DocumentModel dm = session.createEntry(map); assertNotNull(dm); assertEquals("user_0", dm.getId()); String[] schemaNames = dm.getSchemas(); assertEquals(1, schemaNames.length); assertEquals(SCHEMA, schemaNames[0]); assertEquals("user_0", dm.getProperty(SCHEMA, "username")); assertEquals("pass_0", dm.getProperty(SCHEMA, "password")); assertEquals(Long.valueOf(5), dm.getProperty(SCHEMA, "intField")); assertCalendarEquals(getCalendar(1982, 3, 25, 16, 30, 47, 0), (Calendar) dm.getProperty(SCHEMA, "dateField")); List<String> groups = (List<String>) dm.getProperty(SCHEMA, "groups"); assertEquals(2, groups.size()); assertTrue(groups.contains("administrators")); assertTrue(groups.contains("members")); } // recheck the created entries has really been created from a second // session try (Session session = getSession()) { DocumentModel dm = session.getEntry("user_0"); assertNotNull(dm); assertEquals("user_0", dm.getId()); String[] schemaNames = dm.getSchemas(); assertEquals(1, schemaNames.length); assertEquals(SCHEMA, schemaNames[0]); assertEquals("user_0", dm.getProperty(SCHEMA, "username")); assertEquals(Long.valueOf(5), dm.getProperty(SCHEMA, "intField")); assertCalendarEquals(getCalendar(1982, 3, 25, 16, 30, 47, 0), (Calendar) dm.getProperty(SCHEMA, "dateField")); List<String> groups = (List<String>) dm.getProperty(SCHEMA, "groups"); assertEquals(2, groups.size()); assertTrue(groups.contains("administrators")); assertTrue(groups.contains("members")); // password check assertTrue(session.authenticate("user_0", "pass_0")); } } @SuppressWarnings("unchecked") @Test public void testGetEntry() throws Exception { try (Session session = getSession()) { DocumentModel dm = session.getEntry("user_1"); assertEquals("user_1", dm.getProperty(SCHEMA, "username")); assertEquals(Long.valueOf(3), dm.getProperty(SCHEMA, "intField")); assertCalendarEquals(getCalendar(2007, 9, 7, 14, 36, 28, 0), (Calendar) dm.getProperty(SCHEMA, "dateField")); assertNull(dm.getProperty(SCHEMA, "company")); List<String> groups = (List<String>) dm.getProperty(SCHEMA, "groups"); assertEquals(2, groups.size()); assertTrue(groups.contains("group_1")); assertTrue(groups.contains("members")); dm = session.getEntry("Administrator"); assertEquals("Administrator", dm.getProperty(SCHEMA, "username")); assertEquals(Long.valueOf(10), dm.getProperty(SCHEMA, "intField")); assertCalendarEquals(getCalendar(1982, 3, 25, 16, 30, 47, 123), (Calendar) dm.getProperty(SCHEMA, "dateField")); assertTrue((Boolean) dm.getProperty(SCHEMA, "booleanField")); groups = (List<String>) dm.getProperty(SCHEMA, "groups"); assertEquals(1, groups.size()); assertTrue(groups.contains("administrators")); // assertTrue(groups.contains("members")); } } @Test public void testGetEntries() throws Exception { try (Session session = getSession()) { DocumentModelList entries = session.getEntries(); assertEquals(3, entries.size()); Map<String, DocumentModel> entryMap = new HashMap<String, DocumentModel>(); for (DocumentModel entry : entries) { entryMap.put(entry.getId(), entry); } DocumentModel dm = entryMap.get("user_1"); assertNotNull(dm); assertEquals("user_1", dm.getProperty(SCHEMA, "username")); assertCalendarEquals(getCalendar(2007, 9, 7, 14, 36, 28, 0), (Calendar) dm.getProperty(SCHEMA, "dateField")); assertEquals(Long.valueOf(3), dm.getProperty(SCHEMA, "intField")); assertTrue((Boolean) dm.getProperty(SCHEMA, "booleanField")); // XXX: getEntries does not fetch references anymore => groups is // null dm = entryMap.get("Administrator"); assertNotNull(dm); assertEquals("Administrator", dm.getProperty(SCHEMA, "username")); assertEquals(Long.valueOf(10), dm.getProperty(SCHEMA, "intField")); assertCalendarEquals(getCalendar(1982, 3, 25, 16, 30, 47, 123), (Calendar) dm.getProperty(SCHEMA, "dateField")); dm = entryMap.get("user_3"); assertFalse((Boolean) dm.getProperty(SCHEMA, "booleanField")); } try (Session session = directoryService.open(GROUP_DIR)){ DocumentModel doc = session.getEntry("administrators"); assertEquals("administrators", doc.getPropertyValue("group:groupname")); assertEquals("Administrators group", doc.getPropertyValue("group:grouplabel")); doc = session.getEntry("group_1"); assertEquals("group_1", doc.getPropertyValue("group:groupname")); Serializable label = doc.getPropertyValue("group:grouplabel"); if (label != null) { // NULL for Oracle assertEquals("", label); } } } @SuppressWarnings("unchecked") @Test public void testUpdateEntry() throws Exception { try (Session session = getSession()) { DocumentModel dm = session.getEntry("user_1"); // update entry dm.setProperty(SCHEMA, "username", "user_2"); dm.setProperty(SCHEMA, "password", "pass_2"); dm.setProperty(SCHEMA, "intField", Long.valueOf(2)); dm.setProperty(SCHEMA, "dateField", getCalendar(2001, 2, 3, 4, 5, 6, 7)); dm.setProperty(SCHEMA, "groups", Arrays.asList("administrators", "members")); session.updateEntry(dm); } // retrieve entry again // even if we tried to change the user id (username), it should // not be changed try (Session session = getSession()) { DocumentModel dm = session.getEntry("user_1"); assertEquals("user_1", dm.getProperty(SCHEMA, "username")); assertEquals(Long.valueOf(2), dm.getProperty(SCHEMA, "intField")); assertCalendarEquals(getCalendar(2001, 2, 3, 4, 5, 6, 7), (Calendar) dm.getProperty(SCHEMA, "dateField")); List<String> groups = (List<String>) dm.getProperty(SCHEMA, "groups"); assertEquals(2, groups.size()); assertTrue(groups.contains("administrators")); assertTrue(groups.contains("members")); // password check assertTrue(session.authenticate("user_1", "pass_2")); // the user_2 username change was ignored assertNull(session.getEntry("user_2")); // change other field, check password still ok dm.setProperty(SCHEMA, "company", "foo"); session.updateEntry(dm); } try (Session session = getSession()) { // password check assertTrue(session.authenticate("user_1", "pass_2")); } } @SuppressWarnings("unchecked") @Test public void testDeleteEntry() throws Exception { try (Session session = getSession()) { DocumentModel dm = session.getEntry("user_1"); session.deleteEntry(dm); } try (Session session = getSession()) { DocumentModel dm = session.getEntry("user_1"); assertNull(dm); } try (Session session = directoryService.open(GROUP_DIR)) { DocumentModel group1 = session.getEntry("group_1"); List<String> members = (List<String>) group1.getProperty("group", "members"); assertTrue(members.isEmpty()); // assertFalse(members.contains("group_1")); } } // XXX AT: disabled because SQL directories do not accept anymore creation // of a second entry with the same id. The goal here is to accept an entry // with an existing id, as long as parent id is different - e.g full id is // the (parent id, id) tuple. But this constraint does not appear the // directory configuration, so drop it for now. @Test @Ignore public void testDeleteEntryExtended() throws Exception { try (Session session = getSession()) { // create a second entry with user_1 as key but with // a different email (would be "parent" in a hierarchical // vocabulary) Map<String, Object> entryMap = new HashMap<String, Object>(); entryMap.put("username", "user_1"); entryMap.put("email", "second@email"); DocumentModel dm = session.createEntry(entryMap); assertNotNull(dm); assertEquals(3, session.getEntries().size()); // delete with nonexisting email Map<String, String> map = new HashMap<String, String>(); map.put("email", "nosuchemail"); session.deleteEntry("user_1", map); // still there assertEquals(3, session.getEntries().size()); // delete just one map.put("email", "e@m"); session.deleteEntry("user_1", map); // two more entries left assertEquals(2, session.getEntries().size()); // other user_1 still present dm = session.getEntry("user_1"); assertEquals("second@email", dm.getProperty(SCHEMA, "email")); // delete it with a WHERE on a null key map.clear(); map.put("company", null); session.deleteEntry("user_1", map); // entry is gone, only Administrator left assertEquals(1, session.getEntries().size()); } } @SuppressWarnings("unchecked") @Test public void testQuery1() throws Exception { try (Session session = getSession()) { Map<String, Serializable> filter = new HashMap<String, Serializable>(); filter.put("username", "user_1"); filter.put("firstName", "f"); DocumentModelList list = session.query(filter); assertEquals(1, list.size()); DocumentModel docModel = list.get(0); assertNotNull(docModel); assertEquals("user_1", docModel.getProperty(SCHEMA, "username")); assertEquals("f", docModel.getProperty(SCHEMA, "firstName")); // simple query does not fetch references by default => restart with // an explicit fetch request List<String> groups = (List<String>) docModel.getProperty(SCHEMA, "groups"); assertTrue(groups.isEmpty()); list = session.query(filter, null, null, true); assertEquals(1, list.size()); docModel = list.get(0); assertNotNull(docModel); assertEquals("user_1", docModel.getProperty(SCHEMA, "username")); assertEquals("f", docModel.getProperty(SCHEMA, "firstName")); // test that the groups (reference) of user_1 were fetched as well groups = (List<String>) docModel.getProperty(SCHEMA, "groups"); assertEquals(2, groups.size()); assertTrue(groups.contains("members")); assertTrue(groups.contains("group_1")); } } @Test public void testQuerySubAny() throws Exception { try (Session session = getSession()) { Map<String, Serializable> filter = new HashMap<String, Serializable>(); filter.put("username", "er_"); Set<String> set = new HashSet<String>(); set.add("username"); DocumentModelList list = session.query(filter, set); assertEquals(2, list.size()); Set<String> usernames = new HashSet<>(); for (DocumentModel docModel : list) { usernames.add((String) docModel.getProperty(SCHEMA, "username")); } Set<String> expectedUsernames = new HashSet<>(Arrays.asList("user_1", "user_3")); assertEquals(expectedUsernames, usernames); } } @Test public void testQuery2() throws Exception { try (Session session = getSession()) { Map<String, Serializable> filter = new HashMap<String, Serializable>(); filter.put("username", "user_1"); filter.put("email", "nosuchemail"); DocumentModelList list = session.query(filter); assertEquals(0, list.size()); } } @Test public void testQueryCaseInsensitive() throws Exception { try (Session session = getSession()) { Map<String, Serializable> filter = new HashMap<String, Serializable>(); // case insensitive substring search filter.put("username", "admini"); DocumentModelList list = session.query(filter, filter.keySet()); assertEquals(1, list.size()); DocumentModel dm = list.get(0); assertEquals("Administrator", dm.getProperty(SCHEMA, "username")); } } @Test public void testGetProjection() throws Exception { try (Session session = getSession()) { Map<String, Serializable> filter = new HashMap<String, Serializable>(); filter.put("username", "user_1"); List<String> list = session.getProjection(filter, "firstName"); assertEquals(1, list.size()); assertTrue(list.contains("f")); } } @Test public void testSearch() throws Exception { try (Session session = getSession()) { Map<String, Serializable> filter = new HashMap<String, Serializable>(); // exact match filter.put("username", "u"); List<String> users = session.getProjection(filter, "username"); assertEquals(0, users.size()); // substring match users = session.getProjection(filter, filter.keySet(), "username"); assertEquals(2, users.size()); assertTrue(users.contains("user_1")); filter.put("username", "v"); users = session.getProjection(filter, filter.keySet(), "username"); assertEquals(0, users.size()); // trying to cheat filter.put("username", "*"); users = session.getProjection(filter, "username"); assertEquals(0, users.size()); // substring with empty key filter.put("username", ""); users = session.getProjection(filter, filter.keySet(), "username"); assertEquals(3, users.size()); assertTrue(users.contains("user_1")); assertTrue(users.contains("Administrator")); } } @Test public void testIsAuthenticating() throws Exception { try (Session session = getSession()) { // by default the user directory is authenticating assertTrue(session.isAuthenticating()); // by setting a password field that does not belong to the // user SCHEMA, we disable that feature SQLDirectoryDescriptor config = getSQLDirectory().getDescriptor(); try { config.passwordField = "SomeStrangePassordField"; assertFalse(session.isAuthenticating()); } finally { config.passwordField = "password"; } } } @Test public void testAuthenticate() throws Exception { try (Session session = getSession()) { // successful authentication assertTrue(session.authenticate("Administrator", "Administrator")); assertTrue(session.authenticate("user_1", "pass_1")); // authentication against encrypted password assertTrue(session.authenticate("user_3", "pass_3")); // failed authentication: bad password assertFalse(session.authenticate("Administrator", "WrongPassword")); assertFalse(session.authenticate("user", ".asdf'23423")); // failed authentication: not existing user assertFalse(session.authenticate("NonExistingUser", "whatever")); } } @Test public void testCreateFromModel() throws Exception { try (Session session = getSession()) { String schema = "user"; DocumentModel entry = BaseSession.createEntryModel(null, schema, null, null); entry.setProperty("user", "username", "yo"); assertNull(session.getEntry("yo")); session.createEntry(entry); assertNotNull(session.getEntry("yo")); // create one with existing same id, must fail entry.setProperty("user", "username", "Administrator"); try { assertTrue(session.hasEntry("Administrator")); entry = session.createEntry(entry); session.getEntry("Administrator"); fail("Should raise an error, entry already exists"); } catch (DirectoryException e) { } } } @Test public void testHasEntry() throws Exception { try (Session session = getSession()) { assertTrue(session.hasEntry("Administrator")); assertFalse(session.hasEntry("foo")); } } @Ignore @Test @LocalDeploy("org.nuxeo.ecm.directory.sql.tests:test-sql-directories-alteration-config.xml") public void testColumnCreation() throws Exception { AbstractDirectory dirtmp1 = null; AbstractDirectory dirtmp2 = null; try { dirtmp1 = (AbstractDirectory) directoryService.getDirectory("tmpdirectory1"); assertNotNull(dirtmp1); Session session = dirtmp1.getSession(); String schema1 = "tmpschema1"; DocumentModel entry = BaseSession.createEntryModel(null, schema1, null, null); entry.setProperty(schema1, "id", "john"); entry.setProperty(schema1, "label", "monLabel"); assertNull(session.getEntry("john")); entry = session.createEntry(entry); assertEquals("john", entry.getId()); assertNotNull(session.getEntry("john")); // Open a new directory that uses the same table with a different // schema. // And test if the table has not been re-created, and data are there dirtmp2 = (AbstractDirectory) directoryService.getDirectory("tmpdirectory2"); assertNotNull(dirtmp2); session = dirtmp2.getSession(); assertNotNull(session.getEntry("john")); } finally { if (dirtmp1 != null) { dirtmp1.shutdown(); } if (dirtmp2 != null) { dirtmp2.shutdown(); } } } @Test public void testPasswordUpdate() throws Exception { try (Session session = getSession()) { String username = "myuser"; String password = "MyPassword:)"; String password2 = "SomeOtherPassword-"; // create entry Map<String, Object> map = new HashMap<>(); map.put("username", username); map.put("password", password); session.createEntry(map); // check authentication assertTrue(session.authenticate(username, password)); // password has been hashed String pw = ((SQLSession) session).getPassword(username); assertFalse(StringUtils.isBlank(pw)); assertNotSame(password, pw); // different because hashed // change password DocumentModel dm = session.getEntry(username); dm.setProperty(SCHEMA, "password", password2); session.updateEntry(dm); // check authentication with new password assertTrue(session.authenticate(username, password2)); // changing other field dm = session.getEntry(username); dm.setProperty(SCHEMA, "company", "my company"); session.updateEntry(dm); // entry was updated dm = session.getEntry(username); assertEquals("my company", dm.getProperty(SCHEMA, "company")); // check authentication still works, password was not cleared assertTrue(session.authenticate(username, password2)); // setting password to empty is also ignored dm = session.getEntry(username); dm.setProperty(SCHEMA, "company", "unemployed"); dm.setProperty(SCHEMA, "password", ""); session.updateEntry(dm); // entry was updated dm = session.getEntry(username); assertEquals("unemployed", dm.getProperty(SCHEMA, "company")); // check authentication still works, password was not cleared assertTrue(session.authenticate(username, password2)); } } @Test public void testPasswordIgnoredInQueryFilter() throws Exception { try (Session session = getSession()) { Map<String, Serializable> filter = new HashMap<String, Serializable>(); filter.put("username", "user_1"); filter.put("password", "nosuchpassword"); DocumentModelList list = session.query(filter); // returns the result of the query filtered by username, ignoring password assertEquals(1, list.size()); } } @Test public void testPasswordNotReturned() throws Exception { String username = "myuser"; String password = "MyPassword:)"; // create entry try (Session session = getSession()) { Map<String, Object> map = new HashMap<>(); map.put("username", username); map.put("password", password); session.createEntry(map); // check authentication assertTrue(session.authenticate(username, password)); } try (Session session = getSession()) { // password not returned by getEntry DocumentModel dm = session.getEntry(username); String pw = (String) dm.getProperty(SCHEMA, "password"); assertNull(pw); // password not returned by query List<DocumentModel> docs = session.query(Collections.singletonMap("username", username)); assertEquals(1, docs.size()); dm = docs.get(0); pw = (String) dm.getProperty(SCHEMA, "password"); assertNull(pw); } // however is readAllColumns is set, the password is returned (used for test framework) try (Session session = getSession()) { session.setReadAllColumns(true); // password returned by getEntry DocumentModel dm = session.getEntry(username); String pw = (String) dm.getProperty(SCHEMA, "password"); assertFalse(StringUtils.isBlank(pw)); // hashed password PasswordHelper.verifyPassword(password, pw); // password returned by query List<DocumentModel> docs = session.query(Collections.singletonMap("username", username)); assertEquals(1, docs.size()); dm = docs.get(0); pw = (String) dm.getProperty(SCHEMA, "password"); assertFalse(StringUtils.isBlank(pw)); // hashed password PasswordHelper.verifyPassword(password, pw); } } }