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
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ public interface SSOService {
*/
List<SSOUserWithActions> getUsersWithActions(GetUsersWithActionsRequest request);

/**
* Updates the OTP type of a user.
* <p>
* An unknown OTP type code is rejected. Setting type 'none' for a user whose OTP
* is mandatory (is_otp_optional = false) is rejected with a conflict error: make
* OTP optional first.
*/
void updateUserOtpType(UpdateUserOtpTypeRequest request);

/**
Expand Down Expand Up @@ -108,6 +115,13 @@ void registerUser(UserRegisterRequest request)
*/
String getUrlToGoogleAuthQrCode(GetGoogleAuthQrCodeRequest request);

/**
* Updates the "is OTP optional" flag of a user.
* <p>
* When OTP is made mandatory (isOtpOptional = false) and the user's OTP type is
* 'none' or not set, the OTP type is automatically set to 'google_auth', so this
* call may change not only the flag.
*/
void updateUserIsOtpOptionalValue(UpdateUserIsOtpOptionalValueRequest request);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ExceptionSerializationHelper {
registerExceptionClass(SsoUserException.class);
registerExceptionClass(SsoSystemException.class);
registerExceptionClass(SsoDataException.class);
registerExceptionClass(SsoBadRequestException.class);
registerExceptionClass(SsoConflictException.class);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.payneteasy.superfly.model.ui.user;

import com.payneteasy.superfly.api.OTPType;

/**
* The rule "OTP mandatory =&gt; OTP type is not none", in one place.
* <p>
* The service layer applies it on save, and the admin pages apply it in the form so that
* the administrator sees the same outcome before saving. Keeping the default type and the
* condition here stops the two layers from drifting apart.
*/
public final class OtpTypeDefaults {

/** OTP type assigned when OTP is mandatory but no type has been chosen. */
public static final OTPType MANDATORY_DEFAULT = OTPType.GOOGLE_AUTH;

private OtpTypeDefaults() {
}

/**
* @return true when the user would end up with mandatory OTP and no OTP type,
* which the authentication code silently treats as "no second factor"
*/
public static boolean needsDefaultType(UIUser user) {
return !user.isOtpOptional() && OTPType.fromCode(user.getOtpType()) == OTPType.NONE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ public class UIUser implements Serializable {
private String salt;
private String publicKey;
private String otpType = OTPType.NONE.code();
private boolean isOtpOptional;
/**
* Mirrors the DB default {@code is_otp_optional='Y'}: a user built in code and not
* loaded from the DB must not silently end up with mandatory OTP (which would now
* also force an OTP type on them).
*/
private boolean isOtpOptional = true;
private UISubsystemForFilter subsystemForEmail;
private String organization;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.payneteasy.superfly.api.OTPType;
import com.payneteasy.superfly.api.exceptions.PolicyValidationException;
import com.payneteasy.superfly.api.exceptions.SsoBadRequestException;
import com.payneteasy.superfly.api.exceptions.SsoConflictException;
import com.payneteasy.superfly.api.exceptions.SsoDecryptException;
import com.payneteasy.superfly.dao.DaoConstants;
import com.payneteasy.superfly.dao.UserDao;
Expand Down Expand Up @@ -165,8 +167,12 @@ public UIUserDetails getUser(long userId) {
public RoutineResult updateUser(UIUser user) {
UIUserForCreate userForDao = new UIUserForCreate();
copyUserAndEncryptPassword(user, userForDao);
boolean otpTypeAssigned = assignDefaultOtpTypeIfOtpMandatory(userForDao);
// password and salt are not updated here
RoutineResult result = userDao.updateUser(userForDao);
if (otpTypeAssigned) {
loggerSink.info(logger, "AUTO_SET_OTP_TYPE", result.isOk(), user.getUsername());
}
loggerSink.info(logger, "UPDATE_USER", result.isOk(), user.getUsername());
return result;
}
Expand Down Expand Up @@ -462,7 +468,12 @@ public String getUserSalt(String userName) {

@Override
public RoutineResult createUser(UIUserForCreate user) {
return userDao.createUser(user);
boolean otpTypeAssigned = assignDefaultOtpTypeIfOtpMandatory(user);
RoutineResult result = userDao.createUser(user);
if (otpTypeAssigned) {
loggerSink.info(logger, "AUTO_SET_OTP_TYPE", result.isOk(), user.getUsername());
}
return result;
}

@Override
Expand Down Expand Up @@ -556,12 +567,60 @@ public void clearHOTPLoginsFailed(String username) {

@Override
public void updateUserOtpType(String username, String otpType) {
userDao.updateUserOtpType(username,otpType);
OTPType newOtpType = normalizeOtpType(otpType);
if (newOtpType == OTPType.NONE) {
UserForDescription user = userDao.getUserForDescription(username);
if (user != null && !user.isOtpOptional()) {
throw new SsoConflictException("Cannot set OTP type 'none' for user '" + username
+ "': OTP is mandatory for this user, make OTP optional first");
}
}
userDao.updateUserOtpType(username, newOtpType.code());
}

@Override
public void updateUserIsOtpOptionalValue(String username, boolean isOtpOptional) {
userDao.updateUserIsOtpOptionalValue(username,isOtpOptional);
if (!isOtpOptional) {
UserForDescription user = userDao.getUserForDescription(username);
if (user != null && user.getOtpType() == OTPType.NONE) {
userDao.updateUserOtpType(username, OtpTypeDefaults.MANDATORY_DEFAULT.code());
loggerSink.info(logger, "AUTO_SET_OTP_TYPE", true, username);
}
}
}

/**
* Authentication code enforces mandatory OTP only when the user has a concrete OTP type,
* so a user with mandatory OTP must never remain with type 'none'/null.
*
* @return true if the OTP type was assigned, so that the caller can audit it
* together with the result of the DAO call
*/
private boolean assignDefaultOtpTypeIfOtpMandatory(UIUser user) {
// reject an unknown code instead of letting it pass as "no OTP" below
normalizeOtpType(user.getOtpType());

if (!OtpTypeDefaults.needsDefaultType(user)) {
return false;
}
user.setOtpType(OtpTypeDefaults.MANDATORY_DEFAULT.code());
return true;
}

/**
* Maps an OTP type code to the enum: an absent or blank code means "no OTP",
* an unknown code is rejected instead of being silently stored as no OTP.
*/
private static OTPType normalizeOtpType(String otpTypeCode) {
if (otpTypeCode == null || otpTypeCode.trim().isEmpty()) {
return OTPType.NONE;
}
try {
return OTPType.strictFromCode(otpTypeCode);
} catch (IllegalStateException e) {
throw new SsoBadRequestException("Unknown OTP type code: '" + otpTypeCode + "'");
}
}

@Override
Expand Down
Loading
Loading