diff --git a/api/src/main/java/org/openmrs/api/UserService.java b/api/src/main/java/org/openmrs/api/UserService.java index 932f261206b7..a6ff1dc70c11 100644 --- a/api/src/main/java/org/openmrs/api/UserService.java +++ b/api/src/main/java/org/openmrs/api/UserService.java @@ -578,4 +578,13 @@ public List getUsers(String name, List roles, boolean includeRetired * @since 2.3.6, 2.4.6, 2.5.4, 2.6.0 */ Locale getDefaultLocaleForUser(User user); + + /** + * Retrieves the last login time of the user in Unix Timestamp + * + * @param user the subject user + * @return timestamp representing last login time (e.g. 1717414410587) + * @since 2.7.0 + */ + String getLastLoginTime(User user); } diff --git a/api/src/main/java/org/openmrs/api/db/UserDAO.java b/api/src/main/java/org/openmrs/api/db/UserDAO.java index 32beeff4dc63..f3fb80beb543 100644 --- a/api/src/main/java/org/openmrs/api/db/UserDAO.java +++ b/api/src/main/java/org/openmrs/api/db/UserDAO.java @@ -211,4 +211,9 @@ public List getUsers(String name, List roles, boolean includeRetired * @see UserService#setUserActivationKey(LoginCredential) */ public void setUserActivationKey(LoginCredential credentials); + + /** + * @see UserService#getLastLoginTime(User) + */ + String getLastLoginTime(User user); } diff --git a/api/src/main/java/org/openmrs/api/db/hibernate/HibernateContextDAO.java b/api/src/main/java/org/openmrs/api/db/hibernate/HibernateContextDAO.java index addccd760f74..b01a8deadbeb 100644 --- a/api/src/main/java/org/openmrs/api/db/hibernate/HibernateContextDAO.java +++ b/api/src/main/java/org/openmrs/api/db/hibernate/HibernateContextDAO.java @@ -143,7 +143,7 @@ public User authenticate(String login, String password) throws ContextAuthentica // to now and make them wait another x mins final Long unlockTime = getUnlockTimeMs(); if (System.currentTimeMillis() - lockoutTime > unlockTime) { - candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, "0"); + candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, OpenmrsConstants.ZERO_LOGIN_ATTEMPTS_VALUE); candidateUser.removeUserProperty(OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP); saveUserProperties(candidateUser); } else { @@ -172,10 +172,11 @@ public User authenticate(String login, String password) throws ContextAuthentica // only clean up if the were some login failures, otherwise all should be clean int attempts = getUsersLoginAttempts(candidateUser); if (attempts > 0) { - candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, "0"); + candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, OpenmrsConstants.ZERO_LOGIN_ATTEMPTS_VALUE); candidateUser.removeUserProperty(OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP); - saveUserProperties(candidateUser); } + setLastLoginTime(candidateUser); + saveUserProperties(candidateUser); // skip out of the method early (instead of throwing the exception) // to indicate that this is the valid user @@ -215,6 +216,13 @@ public User authenticate(String login, String password) throws ContextAuthentica throw new ContextAuthenticationException(errorMsg); } + private void setLastLoginTime(User candidateUser) { + candidateUser.setUserProperty( + OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP, + String.valueOf(System.currentTimeMillis()) + ); + } + private Long getUnlockTimeMs() { String unlockTimeGPValue = Context.getAdministrationService().getGlobalProperty( OpenmrsConstants.GP_UNLOCK_ACCOUNT_WAITING_TIME); diff --git a/api/src/main/java/org/openmrs/api/db/hibernate/HibernateUserDAO.java b/api/src/main/java/org/openmrs/api/db/hibernate/HibernateUserDAO.java index dccd01526616..f28963bb0797 100644 --- a/api/src/main/java/org/openmrs/api/db/hibernate/HibernateUserDAO.java +++ b/api/src/main/java/org/openmrs/api/db/hibernate/HibernateUserDAO.java @@ -368,7 +368,7 @@ private void updateUserPassword(String newHashedPassword, String salt, Integer c // reset lockout changeForUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP, ""); - changeForUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, "0"); + changeForUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, OpenmrsConstants.ZERO_LOGIN_ATTEMPTS_VALUE); saveUser(changeForUser, null); } @@ -710,4 +710,12 @@ private Query createUserSearchQuery(String name, List roles, boolean inclu public void setUserActivationKey(LoginCredential credentials) { sessionFactory.getCurrentSession().merge(credentials); } + + /** + * @see org.openmrs.api.db.UserDAO#getLastLoginTime(org.openmrs.User) + */ + @Override + public String getLastLoginTime(User user) { + return user.getUserProperty(OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP); + } } diff --git a/api/src/main/java/org/openmrs/api/impl/UserServiceImpl.java b/api/src/main/java/org/openmrs/api/impl/UserServiceImpl.java index 6f51cdf0f0c9..9abf2a4ba781 100644 --- a/api/src/main/java/org/openmrs/api/impl/UserServiceImpl.java +++ b/api/src/main/java/org/openmrs/api/impl/UserServiceImpl.java @@ -808,4 +808,11 @@ public void changePasswordUsingActivationKey(String activationKey, String newPas updatePassword(user, newPassword); } + + /** + * @see org.openmrs.api.UserService#getLastLoginTime(User) + */ + public String getLastLoginTime(User user) { + return dao.getLastLoginTime(user); + } } diff --git a/api/src/main/java/org/openmrs/scheduler/tasks/AutoRetireUsersTask.java b/api/src/main/java/org/openmrs/scheduler/tasks/AutoRetireUsersTask.java new file mode 100644 index 000000000000..0d7285a4eeab --- /dev/null +++ b/api/src/main/java/org/openmrs/scheduler/tasks/AutoRetireUsersTask.java @@ -0,0 +1,99 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.scheduler.tasks; + +import org.apache.commons.lang.StringUtils; +import org.openmrs.User; +import org.openmrs.api.UserService; +import org.openmrs.api.context.Context; +import org.openmrs.util.OpenmrsConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * A scheduled task that automatically retires users after the set number of days of inactivity. + * The inactivity duration is set as a global property. + * Documentation + * {@link OpenmrsConstants#GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS} + * + * @since 2.7.0 + */ +public class AutoRetireUsersTask extends AbstractTask { + + private static final Logger log = LoggerFactory.getLogger(AutoRetireUsersTask.class); + private static final String AUTO_RETIRE_REASON = "User retired due to inactivity"; + + /** + * @see org.openmrs.scheduler.tasks.AbstractTask#execute() + */ + @Override + public void execute() { + if (!isExecuting) { + log.debug("Auto-retiring users task Started"); + + startExecuting(); + + try { + UserService userService = Context.getUserService(); + Set usersToRetire = getUsersToRetire(userService); + + usersToRetire.forEach(user -> userService.retireUser(user, AUTO_RETIRE_REASON)); + } catch (Exception e) { + log.error("Error occurred while auto-retiring users: ", e); + } finally { + log.debug("Auto-retiring users task ended"); + stopExecuting(); + } + } + } + + private Set getUsersToRetire(UserService userService) { + final List allUsers = userService.getAllUsers(); + String numberOfDaysToRetire = Context.getAdministrationService().getGlobalProperty(OpenmrsConstants.GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS); + + if (StringUtils.isBlank(numberOfDaysToRetire)) { + return Collections.emptySet(); + } + + long numberOfMillisecondsToRetire = TimeUnit.DAYS.toMillis(Long.parseLong(numberOfDaysToRetire)); + + return allUsers.stream() + .filter(user -> !user.isSuperUser() + && !user.isRetired() + && userInactivityExceedsDaysToRetire(user, numberOfMillisecondsToRetire) + ) + .collect(Collectors.toSet()); + } + + private boolean userInactivityExceedsDaysToRetire(User user, long numberOfMillisecondsToRetire) { + String lastLoginTimeString = Context.getUserService().getLastLoginTime(user); + + if (StringUtils.isNotBlank(lastLoginTimeString)) { + long lastLoginTime = Long.parseLong(lastLoginTimeString); + + return System.currentTimeMillis() - lastLoginTime >= numberOfMillisecondsToRetire; + } else { + Date dateCreated = user.getDateCreated(); + + if (dateCreated != null) { + return System.currentTimeMillis() - dateCreated.getTime() >= numberOfMillisecondsToRetire; + } + } + + return false; + } +} diff --git a/api/src/main/java/org/openmrs/util/OpenmrsConstants.java b/api/src/main/java/org/openmrs/util/OpenmrsConstants.java index c692dea0debc..efafb19c10ed 100644 --- a/api/src/main/java/org/openmrs/util/OpenmrsConstants.java +++ b/api/src/main/java/org/openmrs/util/OpenmrsConstants.java @@ -23,7 +23,6 @@ import liquibase.GlobalConfiguration; import org.apache.commons.io.IOUtils; import org.openmrs.GlobalProperty; -import org.openmrs.api.context.Context; import org.openmrs.api.handler.ExistingVisitAssignmentHandler; import org.openmrs.customdatatype.datatype.BooleanDatatype; import org.openmrs.customdatatype.datatype.FreeTextDatatype; @@ -636,6 +635,11 @@ public static final Collection AUTO_ROLES() { * Global property that stores the base url for the password reset. */ public static final String GP_PASSWORD_RESET_URL = "security.passwordResetUrl"; + + /** + * Global property that stores the number of days for users to be deactivated. + */ + public static final String GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS = "users.numberOfDaysToRetire"; /** * At OpenMRS startup these global properties/default values/descriptions are inserted into the @@ -1185,6 +1189,11 @@ public static final Collection CONCEPT_PROPOSAL_STATES() { * proficientLocales = en_US, en_GB, en, fr_RW */ public static final String USER_PROPERTY_PROFICIENT_LOCALES = "proficientLocales"; + + /** + * Name of the user_property that stores user's last login time + */ + public static final String USER_PROPERTY_LAST_LOGIN_TIMESTAMP = "lastLoginTimestamp"; // Used for differences between windows/linux upload capabilities) // Used for determining where to find runtime properties @@ -1314,9 +1323,12 @@ public static enum PERSON_TYPE { /** Value for the long person name format */ public static final String PERSON_NAME_FORMAT_LONG = "long"; - + // Liquibase Constants public static final String LIQUIBASE_DUPLICATE_FILE_MODE_DEFAULT = GlobalConfiguration.DuplicateFileMode.WARN.name(); + + /** Value for zero login attempts */ + public static final String ZERO_LOGIN_ATTEMPTS_VALUE = "0"; private OpenmrsConstants() { } diff --git a/api/src/test/java/org/openmrs/api/UserServiceTest.java b/api/src/test/java/org/openmrs/api/UserServiceTest.java index af2f6b6324dd..f8246682c4e2 100644 --- a/api/src/test/java/org/openmrs/api/UserServiceTest.java +++ b/api/src/test/java/org/openmrs/api/UserServiceTest.java @@ -10,9 +10,13 @@ package org.openmrs.api; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -32,7 +36,9 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.FieldUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; @@ -45,7 +51,9 @@ import org.openmrs.Role; import org.openmrs.User; import org.openmrs.api.context.Context; +import org.openmrs.api.context.Credentials; import org.openmrs.api.context.UserContext; +import org.openmrs.api.context.UsernamePasswordCredentials; import org.openmrs.api.db.DAOException; import org.openmrs.api.db.LoginCredential; import org.openmrs.api.db.UserDAO; @@ -1326,7 +1334,7 @@ public void saveUserProperty_shouldAddNewPropertyToExistingUserProperties() { Context.authenticate(user.getUsername(), "testUser1234"); final int numberOfUserProperties = user.getUserProperties().size(); - assertEquals(2, user.getUserProperties().size()); + assertEquals(3, user.getUserProperties().size()); final String USER_PROPERTY_KEY = "test-key"; final String USER_PROPERTY_VALUE = "test-value"; @@ -1662,7 +1670,14 @@ public void saveUserProperty_shouldAddANewPropertyWithAVeryLargeStringWithoutRun final String USER_PROPERTY_KEY = liquibase.util.StringUtil.repeat("emrapi.lastViewedPatientIds,",10); final String USER_PROPERTY_VALUE = liquibase.util.StringUtil.repeat("52345",9899); User updatedUser = userService.saveUserProperty(USER_PROPERTY_KEY, USER_PROPERTY_VALUE); - assertEquals(280, updatedUser.getUserProperties().keySet().iterator().next().length()); + + Set emrApiPropertyKeys = updatedUser.getUserProperties() + .keySet() + .stream() + .filter(key -> key.contains("emrapi.lastViewedPatientIds")) + .collect(Collectors.toSet()); + + assertEquals(280, emrApiPropertyKeys.stream().findFirst().orElse("").length()); assertEquals(49495, updatedUser.getUserProperties().get(USER_PROPERTY_KEY).length()); } @@ -1693,6 +1708,38 @@ public void getDefaultLocaleForUser_shouldReturnDefaultLocaleForUserIfAlreadySet assertEquals(Locale.FRENCH, locale); } + @Test + public void getLastLoginTimeForUser_shouldReturnEmptyStringOnLastLoginTimeIfPropertyNotSet() { + User createdUser = createTestUser(); + assertThat(createdUser.getUserProperty(OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP), emptyString()); + assertThat(Context.getUserService().getLastLoginTime(createdUser), emptyString()); + } + + @Test + public void getLastLoginTimeForUser_shouldReturnEmptyStringOnLastLoginTimeIfADifferentUserIsLoggedIn() { + executeDataSet(XML_FILENAME); + User createdUser = createTestUser(); + Context.authenticate(getTestUserCredentials()); + + assertThat(createdUser.getUserProperty(OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP), emptyString()); + assertThat(Context.getUserService().getLastLoginTime(createdUser), emptyString()); + } + + @Test + public void getLastLoginTimeForUser_shouldNotBeEmptyIfUserIsAuthenticated() { + executeDataSet(XML_FILENAME); + User createdUser = createTestUser(); + Context.authenticate(new UsernamePasswordCredentials("bwolfe", "Openmr5xy")); + + assertThat(createdUser.getUserProperty(OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP), notNullValue()); + assertThat(createdUser.getUserProperty(OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP), not(emptyString())); + assertThat(Context.getUserService().getLastLoginTime(createdUser), not(emptyString())); + } + + private Credentials getTestUserCredentials() { + return new UsernamePasswordCredentials("test", "testUser1234"); + } + private User createTestUser() { User u = new User(); u.setPerson(new Person()); diff --git a/api/src/test/java/org/openmrs/scheduler/tasks/AutoRetireUsersTaskTest.java b/api/src/test/java/org/openmrs/scheduler/tasks/AutoRetireUsersTaskTest.java new file mode 100644 index 000000000000..e8dc4b05e5db --- /dev/null +++ b/api/src/test/java/org/openmrs/scheduler/tasks/AutoRetireUsersTaskTest.java @@ -0,0 +1,161 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.scheduler.tasks; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.GlobalProperty; +import org.openmrs.User; +import org.openmrs.api.AdministrationService; +import org.openmrs.api.UserService; +import org.openmrs.api.context.Context; +import org.openmrs.test.jupiter.BaseContextSensitiveTest; +import org.openmrs.util.OpenmrsConstants; + +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AutoRetireUsersTaskTest extends BaseContextSensitiveTest { + private static final long ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; + private static final long TWENTY_THREE_HOURS_IN_MILLISECONDS = 23 * 60 * 60 * 1000; + private static final long TWO_DAYS_IN_MILLISECONDS = 2 * ONE_DAY_IN_MILLISECONDS; + private static final String ONE_DAY_PROPERTY_VALUE = "1"; + private static final String TWO_DAYS_PROPERTY_VALUE = "2"; + private static final String AUTO_RETIRE_REASON = "User retired due to inactivity"; + + private UserService userService; + private AdministrationService administrationService; + private AutoRetireUsersTask autoRetireUsersTask; + + @BeforeEach + public void setup() { + administrationService = Context.getAdministrationService(); + autoRetireUsersTask = new AutoRetireUsersTask(); + userService = Context.getUserService(); + } + + @Test + public void shouldRetireUsersWhoseInactivityExceedNumberOfDaysToRetire() { + administrationService.saveGlobalProperty(new GlobalProperty(OpenmrsConstants.GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS, ONE_DAY_PROPERTY_VALUE)); + + User user = getDefaultUser(); + userService.setUserProperty( + user, + OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP, + String.valueOf((System.currentTimeMillis() - TWO_DAYS_IN_MILLISECONDS)) + ); + + user.getAllRoles().forEach(user::removeRole); + + autoRetireUsersTask.execute(); + + assertTrue(user.isRetired()); + assertEquals(AUTO_RETIRE_REASON, user.getRetireReason()); + } + + @Test + public void shouldNotRetireUsersWhoseInactivityDoNotExceedNumberOfDaysToRetire() { + administrationService.saveGlobalProperty(new GlobalProperty(OpenmrsConstants.GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS, TWO_DAYS_PROPERTY_VALUE)); + + User user = getDefaultUser(); + userService.setUserProperty( + user, + OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP, + String.valueOf((System.currentTimeMillis() - ONE_DAY_IN_MILLISECONDS)) + ); + + user.getAllRoles().forEach(user::removeRole); + + autoRetireUsersTask.execute(); + + assertFalse(user.isRetired()); + } + + @Test + public void shouldNotRetireAlreadyRetiredUsers() { + administrationService.saveGlobalProperty(new GlobalProperty(OpenmrsConstants.GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS, ONE_DAY_PROPERTY_VALUE)); + + final String RETIRE_REASON = "Retire Test users"; + + User retiredUser = userService.getUser(1); + userService.setUserProperty( + retiredUser, + OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP, + String.valueOf((System.currentTimeMillis() - TWO_DAYS_IN_MILLISECONDS)) + ); + + retiredUser.getAllRoles().forEach(retiredUser::removeRole); + + retiredUser.setRetired(true); + retiredUser.setRetireReason(RETIRE_REASON); + + userService.saveUser(retiredUser); + + autoRetireUsersTask.execute(); + + User fetchedUser = userService.getUser(retiredUser.getUserId()); + assertTrue(fetchedUser.isRetired(), "User should remain retired after the task runs"); + assertEquals(RETIRE_REASON, fetchedUser.getRetireReason()); + } + + @Test + public void shouldNotRetireUsersThatAreInactiveSinceCreationAndInactivityDoesNotExceedNumberOfDaysToRetire() { + administrationService.saveGlobalProperty(new GlobalProperty(OpenmrsConstants.GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS, ONE_DAY_PROPERTY_VALUE)); + + User user = getDefaultUser(); + + user.getAllRoles().forEach(user::removeRole); + + user.setDateCreated(new Date(System.currentTimeMillis() - TWENTY_THREE_HOURS_IN_MILLISECONDS)); + + autoRetireUsersTask.execute(); + + assertFalse(user.isRetired()); + } + + @Test + public void shouldRetireUsersThatAreInactiveSinceCreationAndInactivityExceedsNumberOfDaysToRetire() { + administrationService.saveGlobalProperty(new GlobalProperty(OpenmrsConstants.GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS, ONE_DAY_PROPERTY_VALUE)); + + User user = getDefaultUser(); + + user.getAllRoles().forEach(user::removeRole); + + user.setDateCreated(new Date(System.currentTimeMillis() - TWO_DAYS_IN_MILLISECONDS)); + + autoRetireUsersTask.execute(); + + assertTrue(user.isRetired()); + assertEquals(AUTO_RETIRE_REASON, user.getRetireReason()); + } + + @Test + public void shouldNotRetireSuperUsers() { + administrationService.saveGlobalProperty(new GlobalProperty(OpenmrsConstants.GP_NUMBER_OF_DAYS_TO_AUTO_RETIRE_USERS, ONE_DAY_PROPERTY_VALUE)); + + User adminUser = userService.getUser(1); + userService.setUserProperty( + adminUser, + OpenmrsConstants.USER_PROPERTY_LAST_LOGIN_TIMESTAMP, + String.valueOf((System.currentTimeMillis() - TWO_DAYS_IN_MILLISECONDS)) + ); + + autoRetireUsersTask.execute(); + + assertFalse(adminUser.isRetired()); + } + + private User getDefaultUser() { + return userService.getUser(1); + } +}