Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin
Submodule bin added at 9ffa45
Original file line number Diff line number Diff line change
Expand Up @@ -1230,4 +1230,42 @@ public ResponseEntity<?> getUserDetails(@PathVariable("userName") String userNam
}

}

@Operation(summary = "Unlock user account locked due to failed login attempts")
@RequestMapping(value = "/unlockUserAccount", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization")
public String unlockUserAccount(@RequestBody String request) {
OutputResponse response = new OutputResponse();
try {
Long userId = parseUserIdFromRequest(request);
boolean unlocked = iemrAdminUserServiceImpl.unlockUserAccount(userId);
response.setResponse(unlocked ? "User account successfully unlocked" : "User account was not locked");
} catch (Exception e) {
logger.error("Error unlocking user account: " + e.getMessage(), e);
response.setError(e);
}
return response.toString();
}

@Operation(summary = "Get user account lock status")
@RequestMapping(value = "/getUserLockStatus", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization")
public String getUserLockStatus(@RequestBody String request) {
OutputResponse response = new OutputResponse();
try {
Long userId = parseUserIdFromRequest(request);
String lockStatusJson = iemrAdminUserServiceImpl.getUserLockStatusJson(userId);
response.setResponse(lockStatusJson);
} catch (Exception e) {
logger.error("Error getting user lock status: " + e.getMessage(), e);
response.setError(e);
}
return response.toString();
}

private Long parseUserIdFromRequest(String request) throws IEMRException {
JsonObject requestObj = JsonParser.parseString(request).getAsJsonObject();
if (!requestObj.has("userId") || requestObj.get("userId").isJsonNull()) {
throw new IEMRException("userId is required");
}
return requestObj.get("userId").getAsLong();
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/iemr/common/data/users/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ public class User implements Serializable {
@Column(name = "dhistoken")
private String dhistoken;

@Expose
@Column(name = "lock_timestamp")
private Timestamp lockTimestamp;

/*
* protected User() { }
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ UserSecurityQMapping verifySecurityQuestionAnswers(@Param("UserID") Long UserID,

@Query("SELECT u FROM User u WHERE u.userID=5718")
User getAllExistingUsers();

User findByUserID(Long userID);

}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ public List<ServiceRoleScreenMapping> getUserServiceRoleMappingForProvider(Integ

List<User> getUserIdbyUserName(String userName) throws IEMRException;

boolean unlockUserAccount(Long userId) throws IEMRException;

String getUserLockStatusJson(Long userId) throws IEMRException;


}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public class IEMRAdminUserServiceImpl implements IEMRAdminUserService {
private SessionObject sessionObject;
@Value("${failedLoginAttempt}")
private String failedLoginAttempt;
@Value("${account.lock.duration.hours:24}")
private int accountLockDurationHours;
// @Autowired
// private ServiceRoleScreenMappingRepository ;

Expand Down Expand Up @@ -222,7 +224,25 @@ public void setValidator(Validator validator) {

private void checkUserAccountStatus(User user) throws IEMRException {
if (user.getDeleted()) {
throw new IEMRException("Your account is locked or de-activated. Please contact administrator");
if (user.getLockTimestamp() != null) {
long lockTimeMillis = user.getLockTimestamp().getTime();
long currentTimeMillis = System.currentTimeMillis();
long lockDurationMillis = (long) accountLockDurationHours * 60 * 60 * 1000;

if (currentTimeMillis - lockTimeMillis >= lockDurationMillis) {
user.setDeleted(false);
user.setFailedAttempt(0);
user.setLockTimestamp(null);
iEMRUserRepositoryCustom.save(user);
logger.info("User account auto-unlocked after {} hours lock period for user: {}",
accountLockDurationHours, user.getUserName());
return;
} else {
throw new IEMRException(generateLockoutErrorMessage(user.getLockTimestamp()));
}
} else {
throw new IEMRException("Your account is locked or de-activated. Please contact administrator");
}
} else if (user.getStatusID() > 2) {
throw new IEMRException("Your account is not active. Please contact administrator");
}
Expand Down Expand Up @@ -265,32 +285,27 @@ public List<User> userAuthenticate(String userName, String password) throws Exce
checkUserAccountStatus(user);
iEMRUserRepositoryCustom.save(user);
} else if (validatePassword == 0) {
if (user.getFailedAttempt() + 1 < failedAttempt) {
user.setFailedAttempt(user.getFailedAttempt() + 1);
int currentAttempts = (user.getFailedAttempt() != null) ? user.getFailedAttempt() : 0;
if (currentAttempts + 1 < failedAttempt) {
user.setFailedAttempt(currentAttempts + 1);
user = iEMRUserRepositoryCustom.save(user);
logger.warn("User Password Wrong");
throw new IEMRException("Invalid username or password");
} else if (user.getFailedAttempt() + 1 >= failedAttempt) {
user.setFailedAttempt(user.getFailedAttempt() + 1);
} else {
java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis());
user.setFailedAttempt(currentAttempts + 1);
user.setDeleted(true);
user.setLockTimestamp(lockTime);
user = iEMRUserRepositoryCustom.save(user);
logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.",
ConfigProperties.getInteger("failedLoginAttempt"));

throw new IEMRException(
"Invalid username or password. Please contact administrator.");
} else {
user.setFailedAttempt(user.getFailedAttempt() + 1);
user = iEMRUserRepositoryCustom.save(user);
logger.warn("Failed login attempt {} of {} for a user account.",
user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt"));
throw new IEMRException(
"Invalid username or password. Please contact administrator.");
throw new IEMRException(generateLockoutErrorMessage(lockTime));
}
} else {
checkUserAccountStatus(user);
if (user.getFailedAttempt() != 0) {
if (user.getFailedAttempt() != null && user.getFailedAttempt() != 0) {
user.setFailedAttempt(0);
user.setLockTimestamp(null);
user = iEMRUserRepositoryCustom.save(user);
}
}
Expand All @@ -313,6 +328,37 @@ private void resetUserLoginFailedAttempt(User user) throws IEMRException {

}

private String generateLockoutErrorMessage(java.sql.Timestamp lockTimestamp) {
if (lockTimestamp == null) {
return "Your account has been locked. Please contact the administrator.";
}

long lockTimeMillis = lockTimestamp.getTime();
long currentTimeMillis = System.currentTimeMillis();
long lockDurationMillis = (long) accountLockDurationHours * 60 * 60 * 1000;
long unlockTimeMillis = lockTimeMillis + lockDurationMillis;
long remainingMillis = unlockTimeMillis - currentTimeMillis;

if (remainingMillis <= 0) {
return "Your account lock has expired. Please try logging in again.";
}

long remainingHours = remainingMillis / (60 * 60 * 1000);
long remainingMinutes = (remainingMillis % (60 * 60 * 1000)) / (60 * 1000);

String timeMessage;
if (remainingHours > 0 && remainingMinutes > 0) {
timeMessage = String.format("%d hours %d minutes", remainingHours, remainingMinutes);
} else if (remainingHours > 0) {
timeMessage = String.format("%d hours", remainingHours);
} else {
timeMessage = String.format("%d minutes", remainingMinutes);
}

return String.format("Your account has been locked. You can try again in %s, or contact the administrator.", timeMessage);

}

/**
* Super Admin login
*/
Expand Down Expand Up @@ -351,32 +397,27 @@ public User superUserAuthenticate(String userName, String password) throws Excep
iEMRUserRepositoryCustom.save(user);

} else if (validatePassword == 0) {
if (user.getFailedAttempt() + 1 < failedAttempt) {
user.setFailedAttempt(user.getFailedAttempt() + 1);
int currentAttempts = (user.getFailedAttempt() != null) ? user.getFailedAttempt() : 0;
if (currentAttempts + 1 < failedAttempt) {
user.setFailedAttempt(currentAttempts + 1);
user = iEMRUserRepositoryCustom.save(user);
logger.warn("User Password Wrong");
throw new IEMRException("Invalid username or password");
} else if (user.getFailedAttempt() + 1 >= failedAttempt) {
user.setFailedAttempt(user.getFailedAttempt() + 1);
} else {
java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis());
user.setFailedAttempt(currentAttempts + 1);
user.setDeleted(true);
user.setLockTimestamp(lockTime);
user = iEMRUserRepositoryCustom.save(user);
logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.",
ConfigProperties.getInteger("failedLoginAttempt"));

throw new IEMRException(
"Invalid username or password. Please contact administrator.");
} else {
user.setFailedAttempt(user.getFailedAttempt() + 1);
user = iEMRUserRepositoryCustom.save(user);
logger.warn("Failed login attempt {} of {} for a user account.",
user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt"));
throw new IEMRException(
"Invalid username or password. Please contact administrator.");
throw new IEMRException(generateLockoutErrorMessage(lockTime));
}
} else {
checkUserAccountStatus(user);
if (user.getFailedAttempt() != 0) {
if (user.getFailedAttempt() != null && user.getFailedAttempt() != 0) {
user.setFailedAttempt(0);
user.setLockTimestamp(null);
user = iEMRUserRepositoryCustom.save(user);
}
}
Expand Down Expand Up @@ -1205,12 +1246,12 @@ public User getUserById(Long userId) throws IEMRException {
try {
// Fetch user from custom repository by userId
User user = iEMRUserRepositoryCustom.findByUserID(userId);

// Check if user is found
if (user == null) {
throw new IEMRException("User not found with ID: " + userId);
}

return user;
} catch (Exception e) {
// Log and throw custom exception in case of errors
Expand All @@ -1221,7 +1262,90 @@ public User getUserById(Long userId) throws IEMRException {

@Override
public List<User> getUserIdbyUserName(String userName) {

return iEMRUserRepositoryCustom.findByUserName(userName);
}

@Override
public boolean unlockUserAccount(Long userId) throws IEMRException {
try {
User user = iEMRUserRepositoryCustom.findById(userId).orElse(null);

if (user == null) {
throw new IEMRException("User not found with ID: " + userId);
}

if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() != null) {
user.setDeleted(false);
user.setFailedAttempt(0);
user.setLockTimestamp(null);
iEMRUserRepositoryCustom.save(user);
logger.info("Admin manually unlocked user account for userID: {}", userId);
return true;
} else if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() == null) {
throw new IEMRException("User account is deactivated by administrator. Use user management to reactivate.");
} else {
logger.info("User account is not locked for userID: {}", userId);
return false;
}
} catch (IEMRException e) {
throw e;
} catch (Exception e) {
logger.error("Error unlocking user account with ID: " + userId, e);
throw new IEMRException("Error unlocking user account: " + e.getMessage(), e);
}
}
Comment on lines +1268 to +1296
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find controller class and unlockUserAccount endpoint mapping
rg -t java "unlockUserAccount" --context=5 | head -100

Repository: PSMRI/Common-API

Length of output: 4109


🏁 Script executed:

#!/bin/bash
# Search for controller files and classes that might expose this endpoint
find . -type f -name "*Controller*.java" | xargs grep -l "unlockUserAccount" 2>/dev/null

Repository: PSMRI/Common-API

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Search for authorization/security annotations on any method named unlockUserAccount
rg -t java -B10 "unlockUserAccount\(" | grep -E "(@PreAuthorize|@Secured|@RolesAllowed|@RequestMapping|@PostMapping|@GetMapping)" | head -50

Repository: PSMRI/Common-API

Length of output: 273


🏁 Script executed:

#!/bin/bash
# Get the full IEMRAdminController class definition, especially class-level annotations
head -100 ./src/main/java/com/iemr/common/controller/users/IEMRAdminController.java

Repository: PSMRI/Common-API

Length of output: 3862


🏁 Script executed:

#!/bin/bash
# Search for the unlockUserAccount method with more context to see all annotations
rg -t java -B20 "public String unlockUserAccount" ./src/main/java/com/iemr/common/controller/users/IEMRAdminController.java | head -60

Repository: PSMRI/Common-API

Length of output: 1099


🏁 Script executed:

#!/bin/bash
# Search for Spring Security configuration files
find . -type f -name "*Security*Config*.java" -o -name "*WebSecurity*.java" 2>/dev/null | head -20

Repository: PSMRI/Common-API

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for authorization patterns on /user paths
rg -t java "(/user|unlockUserAccount)" --context=3 | grep -E "(PreAuthorize|Secured|RolesAllowed|hasRole|hasAuthority)" | head -30

Repository: PSMRI/Common-API

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check if the unlockUserAccount method or controller does any role verification internally
rg -t java -A30 "public String unlockUserAccount" ./src/main/java/com/iemr/common/controller/users/IEMRAdminController.java

Repository: PSMRI/Common-API

Length of output: 1454


Add admin role authorization to the /unlockUserAccount endpoint.

The endpoint currently only checks for an Authorization header presence but lacks explicit admin role restriction. This is a sensitive operation that should be restricted to admin users only. Add @PreAuthorize("hasRole('ADMIN')") or equivalent role-based access control to the unlockUserAccount method in IEMRAdminController to prevent unauthorized access.

πŸ€– Prompt for AI Agents
In @src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java
around lines 1268 - 1296, The unlock endpoint lacks an admin-only check: add
role-based authorization to the controller method that exposes unlockUserAccount
by annotating the endpoint method in IEMRAdminController with
@PreAuthorize("hasRole('ADMIN')") (or equivalent), ensure the necessary import
for PreAuthorize is added, and confirm method security is enabled (e.g.,
@EnableGlobalMethodSecurity(prePostEnabled = true) or the appropriate
configuration) so only users with the ADMIN role can call the controller method
that invokes IEMRAdminUserServiceImpl.unlockUserAccount.


@Override
public String getUserLockStatusJson(Long userId) throws IEMRException {
try {
User user = iEMRUserRepositoryCustom.findById(userId).orElse(null);
if (user == null) {
throw new IEMRException("User not found with ID: " + userId);
}

org.json.JSONObject status = new org.json.JSONObject();
status.put("userId", user.getUserID());
status.put("userName", user.getUserName());
status.put("failedAttempts", user.getFailedAttempt() != null ? user.getFailedAttempt() : 0);
status.put("statusID", user.getStatusID());

boolean isDeleted = user.getDeleted() != null && user.getDeleted();
boolean isLockedDueToFailedAttempts = isDeleted && user.getLockTimestamp() != null;

status.put("isLocked", isDeleted);
status.put("isLockedDueToFailedAttempts", isLockedDueToFailedAttempts);

if (isLockedDueToFailedAttempts) {
long lockDurationMillis = (long) accountLockDurationHours * 60 * 60 * 1000;
long remainingMillis = (user.getLockTimestamp().getTime() + lockDurationMillis) - System.currentTimeMillis();
boolean lockExpired = remainingMillis <= 0;

status.put("lockExpired", lockExpired);
status.put("lockTimestamp", user.getLockTimestamp().toString());
status.put("remainingTime", lockExpired ? "Lock expired - will unlock on next login" : formatRemainingTime(remainingMillis));
if (!lockExpired) {
status.put("unlockTime", new java.sql.Timestamp(user.getLockTimestamp().getTime() + lockDurationMillis).toString());
}
} else {
status.put("lockExpired", false);
status.put("lockTimestamp", org.json.JSONObject.NULL);
status.put("remainingTime", org.json.JSONObject.NULL);
}

return status.toString();
} catch (IEMRException e) {
throw e;
} catch (Exception e) {
logger.error("Error fetching user lock status with ID: " + userId, e);
throw new IEMRException("Error fetching user lock status: " + e.getMessage(), e);
}
}
Comment on lines +1298 to +1342
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for controller methods calling getUserLockStatusJson
rg -n 'getUserLockStatusJson' --type=java -B5 -A10

Repository: PSMRI/Common-API

Length of output: 4703


🏁 Script executed:

# Search for controller files that might contain lock-related endpoints
fd -t f -name '*Controller*.java' | xargs rg -l 'lock|Lock' --type=java

Repository: PSMRI/Common-API

Length of output: 10259


🏁 Script executed:

# Check for authorization annotations like @Secured, @PreAuthorize, @RolesAllowed on endpoints
rg -n '@Secured|@PreAuthorize|@RolesAllowed|@PermitAll|@DenyAll' --type=java | head -50

Repository: PSMRI/Common-API

Length of output: 42


🏁 Script executed:

# Get the full controller class definition to see if there are class-level security annotations
rg -n 'class IEMRAdminController' --type=java -A 20

Repository: PSMRI/Common-API

Length of output: 4320


🏁 Script executed:

# Look for the exact method with any annotations
rg -B 15 'public String getUserLockStatus\(@RequestBody String request\)' --type=java

Repository: PSMRI/Common-API

Length of output: 1983


🏁 Script executed:

# Check if there's any role-based authorization in place
rg -n 'hasRole|ROLE_ADMIN|@Secured|@PreAuthorize' --type=java | grep -i 'admin\|lock'

Repository: PSMRI/Common-API

Length of output: 42


🏁 Script executed:

# Check what the Authorization header validation actually does
rg -n 'JwtUserIdValidationFilter|HTTPRequestInterceptor' --type=java -A 5 | head -100

Repository: PSMRI/Common-API

Length of output: 7860


Add role-based access control to the lock status endpoint.

The getUserLockStatus endpoint at /getUserLockStatus requires an Authorization header but does not enforce admin-only access. Currently, any authenticated user with a valid JWT token can access sensitive user lock status information including timestamps and remaining lock durations. Add @PreAuthorize("hasRole('ADMIN')") to the controller method or implement equivalent role validation to restrict access to admin users only.


private String formatRemainingTime(long remainingMillis) {
long hours = remainingMillis / (60 * 60 * 1000);
long minutes = (remainingMillis % (60 * 60 * 1000)) / (60 * 1000);
if (hours > 0 && minutes > 0) return String.format("%d hours %d minutes", hours, minutes);
if (hours > 0) return String.format("%d hours", hours);
return String.format("%d minutes", minutes);
}
}
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ quality-Audit-PageSize=5
## max no of failed login attempt
failedLoginAttempt=5

## account lock duration in hours (24 hours = 1 day for auto-unlock)
account.lock.duration.hours=24

#Jwt Token configuration
jwt.access.expiration=28800000
jwt.refresh.expiration=604800000
Expand Down