This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch 4_1_X in repository https://gitbox.apache.org/repos/asf/syncope.git
commit 68338fed482577a01824b8d9990b20f770dfc23a Author: Francesco Chicchiriccò <[email protected]> AuthorDate: Wed Apr 29 08:31:08 2026 +0200 Restoring feature from SYNCOPE-1903 --- .../common/rest/api/service/UserService.java | 16 ++++++++++++ .../org/apache/syncope/core/logic/UserLogic.java | 27 ++++++++++++++++++++ .../org/apache/syncope/core/logic/UserLogicOp.java | 2 ++ .../core/rest/cxf/service/UserServiceImpl.java | 5 ++++ .../org/apache/syncope/fit/core/UserITCase.java | 29 ++++++++++++++++++++++ 5 files changed, 79 insertions(+) diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserService.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserService.java index 8d8fc8fec6..f7cbc3bd6f 100644 --- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserService.java +++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserService.java @@ -34,6 +34,7 @@ import jakarta.ws.rs.PATCH; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -194,4 +195,19 @@ public interface UserService extends AnyService<UserTO> { @Produces({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON }) Response status(@NotNull StatusR updateReq); + + /** + * Provides answer for the security question configured for user matching the given username, if any. + * If provided answer matches the one stored for that user, check completes successfully, + * otherwise an error is returned. + * + * @param username username for which the security answer is provided + * @param securityAnswer actual answer text + */ + @ApiResponses( + @ApiResponse(responseCode = "204", description = "Operation was successful")) + @POST + @Path("verifySecurityAnswer") + @Produces({ MediaType.APPLICATION_JSON }) + void verifySecurityAnswer(@NotNull @QueryParam("username") String username, String securityAnswer); } diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java index eb94b7e0e2..18b4124df8 100644 --- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java +++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java @@ -24,6 +24,8 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.apache.syncope.common.keymaster.client.api.ConfParamOps; +import org.apache.syncope.common.keymaster.client.api.StandardConfParams; +import org.apache.syncope.common.lib.SyncopeClientException; import org.apache.syncope.common.lib.request.PasswordPatch; import org.apache.syncope.common.lib.request.StatusR; import org.apache.syncope.common.lib.request.StringPatchItem; @@ -34,6 +36,7 @@ import org.apache.syncope.common.lib.to.PropagationStatus; import org.apache.syncope.common.lib.to.ProvisioningResult; import org.apache.syncope.common.lib.to.UserTO; import org.apache.syncope.common.lib.types.AnyTypeKind; +import org.apache.syncope.common.lib.types.ClientExceptionType; import org.apache.syncope.common.lib.types.IdRepoEntitlement; import org.apache.syncope.common.lib.types.PatchOperation; import org.apache.syncope.core.persistence.api.EncryptorManager; @@ -289,4 +292,28 @@ public class UserLogic extends AbstractUserLogic implements UserLogicOp { public ProvisioningResult<UserTO> delete(final String key, final boolean nullPriorityAsync) { return doDelete(binder.getUserTO(key), false, nullPriorityAsync); } + + @PreAuthorize("hasRole('" + IdRepoEntitlement.USER_SEARCH + "')") + @Transactional(readOnly = true) + @Override + public void verifySecurityAnswer(final String username, final String securityAnswer) { + if (!confParamOps.get( + AuthContextUtils.getDomain(), StandardConfParams.PASSWORD_RESET_ALLOWED, false, boolean.class)) { + + SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.DelegatedAdministration); + sce.getElements().add("Password reset forbidden by configuration"); + throw sce; + } + + User user = userDAO.findByUsername(username). + orElseThrow(() -> new NotFoundException("User " + username)); + + if (confParamOps.get( + AuthContextUtils.getDomain(), StandardConfParams.PASSWORD_RESET_SECURITY_QUESTION, false, boolean.class) + && (securityAnswer == null || !encryptorManager.getInstance(). + verify(securityAnswer, user.getCipherAlgorithm(), user.getSecurityAnswer()))) { + + throw SyncopeClientException.build(ClientExceptionType.InvalidSecurityAnswer); + } + } } diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogicOp.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogicOp.java index 5ee4333c24..78a78b0909 100644 --- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogicOp.java +++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogicOp.java @@ -27,4 +27,6 @@ import org.apache.syncope.common.lib.to.UserTO; public interface UserLogicOp extends AnyCRUDLogicOp<UserTO, UserCR, UserUR> { ProvisioningResult<UserTO> status(StatusR statusR, boolean nullPriorityAsync); + + void verifySecurityAnswer(String username, String securityAnswer); } diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java index 9dbed5c4fd..2cfd4d152d 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java @@ -82,4 +82,9 @@ public class UserServiceImpl extends AbstractAnyService<UserTO, UserCR, UserUR> ProvisioningResult<UserTO> updated = logic.status(statusR, isNullPriorityAsync()); return modificationResponse(updated); } + + @Override + public void verifySecurityAnswer(final String username, final String securityAnswer) { + logic.verifySecurityAnswer(username, securityAnswer); + } } diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java index db0693a71f..2cafee9305 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java @@ -47,6 +47,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.apache.cxf.jaxrs.client.WebClient; import org.apache.syncope.client.lib.SyncopeClient; import org.apache.syncope.client.lib.batch.BatchRequest; +import org.apache.syncope.common.keymaster.client.api.StandardConfParams; import org.apache.syncope.common.lib.Attr; import org.apache.syncope.common.lib.SyncopeClientException; import org.apache.syncope.common.lib.SyncopeConstants; @@ -1393,4 +1394,32 @@ public class UserITCase extends AbstractITCase { UserTO userTO = createUser(userCR).getEntity(); assertNotNull(userTO.getKey()); } + + @Test + public void verifySecurityAnswer() { + // 0. ensure that password request DOES require security question + confParamOps.set(SyncopeConstants.MASTER_DOMAIN, StandardConfParams.PASSWORD_RESET_SECURITY_QUESTION, true); + + // 1. create an user with security question and answer + UserCR user = UserITCase.getUniqueSample("[email protected]"); + user.setSecurityQuestion("887028ea-66fc-41e7-b397-620d7ea6dfbb"); + user.setSecurityAnswer("Rossi"); + createUser(user); + + // 2. verify wrong security answer + try { + ADMIN_CLIENT.getService(UserService.class).verifySecurityAnswer(user.getUsername(), "WRONG"); + fail("This should not happen"); + } catch (SyncopeClientException e) { + assertEquals(ClientExceptionType.InvalidSecurityAnswer, e.getType()); + } + + // 3. verify the expected security answer + try { + ADMIN_CLIENT.getService(UserService.class).verifySecurityAnswer(user.getUsername(), "Rossi"); + } catch (SyncopeClientException e) { + assertEquals(ClientExceptionType.InvalidSecurityAnswer, e.getType()); + fail("This should not happen"); + } + } }
