Skip to content

Commit

Permalink
AYS-509 | Restricted Fields Have Been Masked while Saving Logs (#423)
Browse files Browse the repository at this point in the history
  • Loading branch information
agitrubard authored Jan 3, 2025
1 parent baaecd3 commit 5859f18
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 18 deletions.
4 changes: 2 additions & 2 deletions src/main/java/org/ays/common/model/AysAuditLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public AysAuditLogBuilder aysHttpServletRequest(final AysHttpServletRequest aysH

this.request.httpMethod = aysHttpServletRequest.getMethod();
this.request.path = aysHttpServletRequest.getPath();
this.request.body = AysJsonUtil.toEscapedJson(aysHttpServletRequest.getBody());
this.request.body = AysJsonUtil.toMaskedEscapedJson(aysHttpServletRequest.getBody());
return this;
}

Expand All @@ -99,7 +99,7 @@ public AysAuditLogBuilder aysHttpHeader(final AysHttpHeader aysHttpHeader) {
public AysAuditLogBuilder aysHttpServletResponse(final AysHttpServletResponse aysHttpServletResponse) {

this.response.httpStatusCode = aysHttpServletResponse.getStatus();
this.response.body = AysJsonUtil.toEscapedJson(aysHttpServletResponse.getBody());
this.response.body = AysJsonUtil.toMaskedEscapedJson(aysHttpServletResponse.getBody());
return this;
}

Expand Down
11 changes: 10 additions & 1 deletion src/main/java/org/ays/common/model/request/AysHttpHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.ays.common.util.AysMaskUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;

Expand Down Expand Up @@ -58,7 +59,15 @@ private String getAllHeaders(final HttpServletRequest httpServletRequest) {
headerNames.remove(X_FORWARDED_FOR.toLowerCase());
return headerNames
.stream()
.map(headerName -> headerName + ":" + httpServletRequest.getHeader(headerName))
.map(headerName -> {

final String headerValue = httpServletRequest.getHeader(headerName);
if (headerName.equalsIgnoreCase(AUTHORIZATION)) {
return headerName + ":" + AysMaskUtil.mask(headerName, headerValue);
}

return headerName + ":" + headerValue;
})
.collect(Collectors.joining("; "));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.ays.common.util.validation.PhoneNumber;

/**
Expand Down Expand Up @@ -35,8 +34,4 @@ public String toString() {
.formatted(this.countryCode, this.lineNumber);
}

public boolean isBlank() {
return StringUtils.isBlank(this.countryCode) && StringUtils.isBlank(this.lineNumber);
}

}
20 changes: 13 additions & 7 deletions src/main/java/org/ays/common/util/AysJsonUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,24 @@ public static String toJson(final Object object) {
}

/**
* Converts a formatted JSON string into an unformatted (compact) JSON string.
* Converts a JSON string into a masked and escaped JSON string.
* <p>
* This method parses the input JSON string into a tree structure using the {@link ObjectMapper}
* and then converts it back to a compact string representation.
* If an exception occurs during parsing, an empty string is returned.
* This method performs the following steps:
* <ul>
* <li>Parses the input JSON string into a tree structure using the {@link ObjectMapper}.</li>
* <li>Masks sensitive fields in the JSON using {@link AysMaskUtil}.</li>
* <li>Escapes double quotes in the JSON string by replacing them with backslash-escaped quotes.</li>
* </ul>
* If an exception occurs during parsing or masking, an empty string is returned.
* </p>
*
* @param json the formatted JSON string to be converted
* @return a compact (unformatted) JSON string, or an empty string if the input is invalid
* @param json The JSON string to be masked and escaped.
* @return A masked and escaped JSON string, or an empty string if the input is invalid.
*/
public static String toEscapedJson(final String json) {
public static String toMaskedEscapedJson(final String json) {
try {
final JsonNode jsonNode = OBJECT_MAPPER.readTree(json);
AysMaskUtil.mask(jsonNode);
return jsonNode.toString().replace("\"", "\\\"");
} catch (JsonProcessingException exception) {
return "";
Expand Down
206 changes: 206 additions & 0 deletions src/main/java/org/ays/common/util/AysMaskUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package org.ays.common.util;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;

import java.util.Iterator;

/**
* Utility class for masking sensitive information in JSON data.
* <p>
* This class provides methods for masking fields within a {@link JsonNode} or individual string values.
* It is designed to handle common sensitive fields such as tokens, passwords, email addresses, and more.
* </p>
*
* <p><strong>Unmasked JSON:</strong></p>
* <pre>
* {
* "emailAddress": "[email protected]",
* "password": "123456789",
* "lineNumber": "1234567890",
* "address": "123 Main Street, Springfield",
* "firstName": "John",
* "lastName": "Doe"
* }
* </pre>
*
* <p><strong>Masked JSON:</strong></p>
* <pre>
* {
* "emailAddress": "tes******com",
* "password": "******",
* "lineNumber": "******7890",
* "address": "123 Main ******field",
* "firstName": "Joh******ohn",
* "lastName": "Doe******oe"
* }
* </pre>
*/
@UtilityClass
public class AysMaskUtil {

private static final String MASKED_VALUE = "******";

/**
* Masks sensitive fields in the given {@link JsonNode}.
* <p>
* This method recursively iterates through the fields in the JSON object or array,
* and applies masking to fields identified as sensitive.
* </p>
*
* @param jsonNode the JSON node to process for masking
*/
public static void mask(final JsonNode jsonNode) {

if (jsonNode.isObject()) {

ObjectNode objectNode = (ObjectNode) jsonNode;
Iterator<String> fieldNames = objectNode.fieldNames();

while (fieldNames.hasNext()) {

String fieldName = fieldNames.next();
JsonNode fieldValue = objectNode.get(fieldName);

if (fieldValue.isValueNode()) {
String maskedValue = mask(fieldName, fieldValue.asText());
objectNode.put(fieldName, maskedValue);
continue;
}

mask(fieldValue);
}

return;
}

if (jsonNode.isArray()) {
ArrayNode arrayNode = (ArrayNode) jsonNode;
for (JsonNode arrayElement : arrayNode) {
mask(arrayElement);
}
}
}

/**
* Masks the value of a specific field based on its name.
* <p>
* This method identifies sensitive fields by their names and applies the appropriate
* masking strategy. Fields not recognized as sensitive are returned without modification.
* </p>
*
* @param field the name of the field
* @param value the value to mask
* @return the masked value
*/
public static String mask(final String field, final String value) {

if ("null".equalsIgnoreCase(value) || StringUtils.isBlank(value)) {
return value;
}

return switch (field) {
case "Authorization", "accessToken", "refreshToken" -> maskToken(value);
case "password" -> maskPassword();
case "emailAddress" -> maskEmailAddress(value);
case "lineNumber" -> maskLineNumber(value);
case "address" -> maskAddress(value);
case "firstName", "lastName", "applicantFirstName", "applicantLastName" -> maskName(value);
default -> value;
};
}

/**
* Masks token fields such as "Authorization", "accessToken", or "refreshToken".
*
* @param value the token value to mask
* @return the masked token
*/
private static String maskToken(String value) {

if (value.length() <= 20) {
return value;
}

return value.substring(0, 20) + MASKED_VALUE;
}

/**
* Masks password fields, replacing their values with a fixed placeholder.
*
* @return the masked password placeholder
*/
private static String maskPassword() {
return MASKED_VALUE;
}

/**
* Masks email addresses by revealing the first three and last three characters,
* replacing the rest with asterisks.
*
* @param value the email address to mask
* @return the masked email address
*/
private static String maskEmailAddress(String value) {

if (value.length() <= 3) {
return value;
}

int length = value.length();
String firstThree = value.substring(0, 3);
String lastThree = value.substring(length - 3);
return firstThree + MASKED_VALUE + lastThree;
}

/**
* Masks address fields by revealing the first ten and last ten characters,
* replacing the middle part with asterisks.
*
* @param value the address to mask
* @return the masked address
*/
private static String maskAddress(String value) {

if (value.length() <= 10) {
return value;
}

return value.substring(0, 10) + MASKED_VALUE + value.substring(value.length() - 10);
}

/**
* Masks line numbers by revealing the last four digits, replacing the preceding digits with asterisks.
*
* @param value the line number to mask
* @return the masked line number
*/
private static String maskLineNumber(String value) {

if (value.length() <= 4) {
return value;
}

return MASKED_VALUE + value.substring(value.length() - 4);
}

/**
* Masks name fields such as "firstName", "lastName", or similar by revealing the first three
* and last three characters, replacing the middle part with asterisks.
*
* @param value the name to mask
* @return the masked name
*/
private static String maskName(String value) {

if (value.length() <= 3) {
return value;
}

return value.substring(0, 3) + MASKED_VALUE + value.substring(value.length() - 3);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ private boolean isAllApplicantFieldsFilled() {
return true;
}


return !StringUtils.isBlank(this.applicantFirstName) && !StringUtils.isBlank(this.applicantLastName)
&&
this.applicantPhoneNumber != null && !this.applicantPhoneNumber.isBlank();
this.applicantPhoneNumber != null
&&
!(StringUtils.isBlank(this.applicantPhoneNumber.getCountryCode()) && StringUtils.isBlank(this.applicantPhoneNumber.getLineNumber()));
}

@JsonIgnore
Expand All @@ -111,15 +114,15 @@ private boolean isPhoneNumberMustNotBeSameOne() {
@AssertTrue(message = "source city/district and target city/district must be different")
@SuppressWarnings("This method is unused by the application directly but Spring is using it in the background.")
private boolean isSourceCityAndDistrictDifferentFromTargetCityAndDistrict() {

if (this.sourceCity == null || this.sourceDistrict == null || this.targetCity == null || this.targetDistrict == null) {
return true;
}

if (!this.sourceCity.equalsIgnoreCase(this.targetCity)){
return true;
}

return !this.sourceDistrict.equalsIgnoreCase(this.targetDistrict);
}

Expand Down

0 comments on commit 5859f18

Please sign in to comment.