Skip to content

Commit

Permalink
TRUNK-6235: auto deactivate users after XX days of inactivity (#4650)
Browse files Browse the repository at this point in the history
* TRUNK-6235 - auto deactivate users

* --- (#4643)

updated-dependencies:
- dependency-name: org.codehaus.mojo:build-helper-maven-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update to Tomcat 9

* --- (#4647)

updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* --- (#4648)

updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy-agent
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Updating tests

* Changes from Pull request

* Reverting refactor

* Adding property in core global property

* Using matchers in place of empty string

* Adding licence

* Adding tests for auto-retire

* Changing an access modifier

* Improving documentation

* Updating tests

* escaping superusers while auto retiring users

* Code refactor

* Improving tests

* Improving tests

* Code refactor

* Using TimeUnit for time conversion

* Changing method modifiers to private

* Fixing a failing build

* making a static variable private

* Using exact retire reason in tests

* Fixing tests

* correcting the expected values

* Removing import of a constant

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Isaiah <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ian <[email protected]>
Co-authored-by: Isaiah Muli <[email protected]>
  • Loading branch information
5 people authored Jun 26, 2024
1 parent cca287a commit 9241104
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 8 deletions.
9 changes: 9 additions & 0 deletions api/src/main/java/org/openmrs/api/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -578,4 +578,13 @@ public List<User> getUsers(String name, List<Role> 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);
}
5 changes: 5 additions & 0 deletions api/src/main/java/org/openmrs/api/db/UserDAO.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,9 @@ public List<User> getUsers(String name, List<Role> roles, boolean includeRetired
* @see UserService#setUserActivationKey(LoginCredential)
*/
public void setUserActivationKey(LoginCredential credentials);

/**
* @see UserService#getLastLoginTime(User)
*/
String getLastLoginTime(User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -710,4 +710,12 @@ private Query createUserSearchQuery(String name, List<Role> 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);
}
}
7 changes: 7 additions & 0 deletions api/src/main/java/org/openmrs/api/impl/UserServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <a href="https://openmrs.atlassian.net/wiki/spaces/docs/pages/101318663/Creating+Auto-Deactivating+User+Task">Documentation</a>
* {@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<User> 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<User> getUsersToRetire(UserService userService) {
final List<User> 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;
}
}
16 changes: 14 additions & 2 deletions api/src/main/java/org/openmrs/util/OpenmrsConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -636,6 +635,11 @@ public static final Collection<String> 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
Expand Down Expand Up @@ -1185,6 +1189,11 @@ public static final Collection<String> CONCEPT_PROPOSAL_STATES() {
* <code>proficientLocales = en_US, en_GB, en, fr_RW</code>
*/
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
Expand Down Expand Up @@ -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() {
}
Expand Down
51 changes: 49 additions & 2 deletions api/src/test/java/org/openmrs/api/UserServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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<String> 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());
}

Expand Down Expand Up @@ -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());
Expand Down
Loading

0 comments on commit 9241104

Please sign in to comment.