Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed Issue in idempotency validation for APIs without a payload #57

Merged
merged 2 commits into from
Apr 26, 2024
Merged
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 @@ -26,6 +26,7 @@ public class IdempotencyConstants {
public static final String X_IDEMPOTENCY_KEY = "x-idempotency-key";
public static final String IDEMPOTENCY_KEY_NAME = "IdempotencyKey";
public static final String ISO_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX";
public static final String EMPTY_OBJECT = "{}";
public static final String ERROR_PAYLOAD_NOT_SIMILAR = "Payloads are not similar. Hence this is not a valid" +
" idempotent request";
public static final String ERROR_AFTER_ALLOWED_TIME = "Request received after the allowed time., Hence this is" +
Expand All @@ -36,4 +37,6 @@ public class IdempotencyConstants {
" is not a valid idempotent request";
public static final String JSON_COMPARING_ERROR = "Error occurred while comparing JSON payloads";
public static final String CONSENT_RETRIEVAL_ERROR = "Error while retrieving detailed consent data";
public static final String SAME_CONSENT_ID_ERROR = "Cannot use different unique identifier for the same" +
" consent ID when the request does not contain a payload.";
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Class to hold idempotency validation utils.
Expand All @@ -49,8 +52,8 @@ public class IdempotencyValidationUtils {
* @param idempotencyKeyValue Idempotency Key Value
* @return List of consent ids if available, else an empty list will be returned
*/
static ArrayList<String> getConsentIdsFromIdempotencyKey(String idempotencyKeyName,
String idempotencyKeyValue) {
static List<String> getConsentIdsFromIdempotencyKey(String idempotencyKeyName,
String idempotencyKeyValue) {
try {
return consentCoreService.getConsentIdByConsentAttributeNameAndValue(
idempotencyKeyName, idempotencyKeyValue);
Expand All @@ -60,6 +63,21 @@ static ArrayList<String> getConsentIdsFromIdempotencyKey(String idempotencyKeyNa
}
}

/**
* Method to retrieve the consent ids and idempotency key value using the idempotency key.
*
* @param idempotencyKeyName Idempotency Key Name
* @return Map of consent ids and idempotency key vallue if available, else an empty map will be returned
*/
static Map<String, String> getAttributesFromIdempotencyKey(String idempotencyKeyName) {
try {
return consentCoreService.getConsentAttributesByName(idempotencyKeyName);
} catch (ConsentManagementException e) {
log.debug("No consent ids found for the idempotency key value");
return new HashMap<>();
}
}

/**
* Method to compare the client ID sent in the request and client id retrieved from the database.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* Class to handle idempotency related operations.
Expand Down Expand Up @@ -81,26 +82,30 @@ public IdempotencyValidationResult validateIdempotency(ConsentManageData consent
}
try {
String idempotencyKeyName = getIdempotencyAttributeName(consentManageData.getRequestPath());
// Retrieve consent ids that have the idempotency key name and value as attribute
ArrayList<String> consentIds = IdempotencyValidationUtils
.getConsentIdsFromIdempotencyKey(idempotencyKeyName, idempotencyKeyValue);
// Check whether the consent id list is not empty. If idempotency key exists in the database then
// the consent Id list will be not empty.
if (!consentIds.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug(String.format("Idempotency Key %s exists in the database. Hence this is an" +
" idempotent request", idempotencyKeyValue));
}
for (String consentId : consentIds) {
DetailedConsentResource consentRequest = consentCoreService.getDetailedConsent(consentId);
if (consentRequest != null) {
return validateIdempotencyConditions(consentManageData, consentRequest);
} else {
String errorMsg = String.format(IdempotencyConstants.ERROR_NO_CONSENT_DETAILS, consentId);
log.error(errorMsg);
throw new IdempotencyValidationException(errorMsg);
if (!IdempotencyConstants.EMPTY_OBJECT.equals(consentManageData.getPayload().toString())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Couldn't this be null? consentManageData.getPayload()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is checked upfront in line 68

aka4rKO marked this conversation as resolved.
Show resolved Hide resolved
// Retrieve consent ids that have the idempotency key name and value as attribute
List<String> consentIds = IdempotencyValidationUtils
.getConsentIdsFromIdempotencyKey(idempotencyKeyName, idempotencyKeyValue);
// Check whether the consent id list is not empty. If idempotency key exists in the database then
// the consent Id list will be not empty.
if (!consentIds.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug(String.format("Idempotency Key %s exists in the database. Hence this is an" +
" idempotent request", idempotencyKeyValue));
}
for (String consentId : consentIds) {
DetailedConsentResource consentResource = consentCoreService.getDetailedConsent(consentId);
if (consentResource != null) {
return validateIdempotencyConditions(consentManageData, consentResource);
} else {
String errorMsg = String.format(IdempotencyConstants.ERROR_NO_CONSENT_DETAILS, consentId);
log.error(errorMsg);
throw new IdempotencyValidationException(errorMsg);
}
}
}
} else {
return validateIdempotencyWithoutPayload(consentManageData, idempotencyKeyName, idempotencyKeyValue);
}
} catch (IOException e) {
log.error(IdempotencyConstants.JSON_COMPARING_ERROR, e);
Expand All @@ -112,6 +117,53 @@ public IdempotencyValidationResult validateIdempotency(ConsentManageData consent
return new IdempotencyValidationResult(false, false);
}

/**
* Method to check whether the idempotency conditions are met for requests without payload.
* This method will validate the following conditions.
* - Whether the idempotency key value is different for the same consent id
* - Whether the client id sent in the request and client id retrieved from the database are equal
* - Whether the difference between two dates is less than the configured time
* - Whether payloads are equal
*
* @param consentManageData Consent Manage Data
* @param idempotencyKeyName Idempotency Key Name
* @param idempotencyKeyValue Idempotency Key value
* @return IdempotencyValidationResult
*/
private IdempotencyValidationResult validateIdempotencyWithoutPayload(ConsentManageData consentManageData,
String idempotencyKeyName,
String idempotencyKeyValue)
throws IdempotencyValidationException, IOException, ConsentManagementException {

// Retrieve consent ids and idempotency key values that have the idempotency key name
Map<String, String> attributes = IdempotencyValidationUtils.getAttributesFromIdempotencyKey(idempotencyKeyName);
// Check whether the attributes map is not empty. If idempotency key exists in the database then
// the consent Id list will be not empty.
if (!attributes.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug(String.format("Idempotency Key %s exists in the database. Hence this is an" +
" idempotent request", idempotencyKeyValue));
}
for (Map.Entry<String, String> entry : attributes.entrySet()) {
// If the idempotency key value is different for the same consent id then it is not a valid idempotent
if (consentManageData.getRequestPath().contains(entry.getKey()) &&
!idempotencyKeyValue.equals(entry.getValue())) {
throw new IdempotencyValidationException(IdempotencyConstants.SAME_CONSENT_ID_ERROR);
}
DetailedConsentResource consentRequest = consentCoreService.getDetailedConsent(entry.getKey());
if (consentRequest != null) {
return validateIdempotencyConditions(consentManageData, consentRequest);
} else {
String errorMsg = String.format(IdempotencyConstants.ERROR_NO_CONSENT_DETAILS, entry.getKey());
log.error(errorMsg);
throw new IdempotencyValidationException(errorMsg);
}
}

}
return new IdempotencyValidationResult(false, false);
}

/**
* Method to check whether the idempotency conditions are met.
* This method will validate the following conditions.
Expand All @@ -120,25 +172,26 @@ public IdempotencyValidationResult validateIdempotency(ConsentManageData consent
* - Whether payloads are equal
*
* @param consentManageData Consent Manage Data
* @param consentRequest Detailed Consent Resource
* @param consentResource Detailed Consent Resource
* @return IdempotencyValidationResult
*/
private IdempotencyValidationResult validateIdempotencyConditions(ConsentManageData consentManageData,
DetailedConsentResource consentRequest)
DetailedConsentResource consentResource)
throws IdempotencyValidationException, IOException {
// Compare the client ID sent in the request and client id retrieved from the database
// to validate whether the request is received from the same client
if (IdempotencyValidationUtils.isClientIDEqual(consentRequest.getClientID(), consentManageData.getClientId())) {
if (IdempotencyValidationUtils.isClientIDEqual(consentResource.getClientID(),
consentManageData.getClientId())) {
// Check whether difference between two dates is less than the configured time
if (IdempotencyValidationUtils.isRequestReceivedWithinAllowedTime(getCreatedTimeOfPreviousRequest(
consentManageData.getRequestPath(), consentRequest.getConsentID()))) {
consentManageData.getRequestPath(), consentResource.getConsentID()))) {
// Compare whether JSON payloads are equal
if (isPayloadSimilar(consentManageData, getPayloadOfPreviousRequest(
consentManageData.getRequestPath(), consentRequest.getConsentID()))) {
consentManageData.getRequestPath(), consentResource.getConsentID()))) {
log.debug("Payloads are similar and request received within allowed" +
" time. Hence this is a valid idempotent request");
return new IdempotencyValidationResult(true, true,
consentRequest, consentRequest.getConsentID());
consentResource, consentResource.getConsentID());
} else {
log.error(IdempotencyConstants.ERROR_PAYLOAD_NOT_SIMILAR);
throw new IdempotencyValidationException(IdempotencyConstants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class IdempotencyValidatorTests extends PowerMockTestCase {
private ConsentManageData consentManageData;
private ConsentCoreServiceImpl consentCoreServiceImpl;
private ArrayList<String> consentIdList;
private Map<String, String> attributeList;
private String consentId;
private Map<String, Object> configs;
private Map<String, String> headers;
Expand Down Expand Up @@ -126,6 +127,9 @@ public void beforeTest() {
consentId = UUID.randomUUID().toString();
consentIdList = new ArrayList<>();
consentIdList.add(consentId);

attributeList = new HashMap<>();
attributeList.put(consentId, "123456");
}

@BeforeMethod
Expand Down Expand Up @@ -164,6 +168,22 @@ public void testValidateIdempotency() throws ConsentManagementException, Idempot
Assert.assertEquals(consentId, result.getConsentId());
}

@Test(expectedExceptions = IdempotencyValidationException.class)
public void testValidateIdempotencyForRequestsWithoutPayload() throws ConsentManagementException,
IdempotencyValidationException {
OffsetDateTime offsetDateTime = OffsetDateTime.now();

Mockito.doReturn(attributeList).when(consentCoreServiceImpl).getConsentAttributesByName(Mockito.anyString());
Mockito.doReturn(getConsent(offsetDateTime.toEpochSecond())).when(consentCoreServiceImpl)
.getDetailedConsent(Mockito.anyString());
Mockito.doReturn(headers).when(consentManageData).getHeaders();
Mockito.doReturn(CLIENT_ID).when(consentManageData).getClientId();
Mockito.doReturn("{}").when(consentManageData).getPayload();
Mockito.doReturn("{}").when(consentManageData).getPayload();
Mockito.doReturn("/payments/".concat(consentId)).when(consentManageData).getRequestPath();
new IdempotencyValidator().validateIdempotency(consentManageData);
}

@Test
public void testValidateIdempotencyWithoutIdempotencyKeyValue() throws IdempotencyValidationException {

Expand Down
Loading