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");
+        }
+    }
 }

Reply via email to