diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/LoggingInterceptor.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/LoggingInterceptor.kt index 4302901e..03bef079 100644 --- a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/LoggingInterceptor.kt +++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/LoggingInterceptor.kt @@ -15,6 +15,7 @@ import java.io.IOException * @param maxBodyLogSize The maximum size of the request/response body to log. Defaults to 1MB. */ class LoggingInterceptor( + private val logger: LoggerDecorator, private val maxBodyLogSize: Long = DEFAULT_MAX_BODY_SIZE, ) : Interceptor { @Throws(IOException::class) diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggerDecorator.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggerDecorator.kt index fe4c76ca..f969ff65 100644 --- a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggerDecorator.kt +++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/common/LoggerDecorator.kt @@ -4,6 +4,7 @@ import org.slf4j.Logger class LoggerDecorator( private val logger: Logger, + private val masker: (String) -> String = { it }, ) : Logger by logger { override fun info(msg: String) = logger.info(decorate(msg)) diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldFilter.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldFilter.kt deleted file mode 100644 index 5936601e..00000000 --- a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldFilter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.expediagroup.sdk.core.logging.masking - -import com.ebay.ejmask.core.BaseFilter - -/** - * A filter class that extends the BaseFilter to apply masking on specific JSON fields using - * `ExpediaGroupJsonFieldPatternBuilder` for pattern building. - * - * This filter helps in masking sensitive JSON fields by replacing them with a predefined pattern. - * - * @constructor - * Initializes ExpediaGroupJsonFieldFilter with the specified fields to be masked. - * - * @param maskedFields An array of strings representing the names of the fields to be masked. - */ -internal class JsonFieldFilter( - maskedFields: Array, -) : BaseFilter( - JsonFieldPatternBuilder::class.java, - *maskedFields, - ) diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldPatternBuilder.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldPatternBuilder.kt deleted file mode 100644 index e8cec9fc..00000000 --- a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/JsonFieldPatternBuilder.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.expediagroup.sdk.core.logging.masking - -import com.ebay.ejmask.extenstion.builder.json.JsonFieldPatternBuilder -import com.expediagroup.sdk.core.logging.common.Constant.OMITTED - -/** - * A builder class for creating JSON field replacement patterns specifically for Expedia Group. - * - * This class extends the `JsonFieldPatternBuilder` and provides an implementation for building - * replacement patterns for JSON field masking. - * - * The replacement pattern format generated by this builder is structured to conceal sensitive - * data while keeping a specified number of characters visible. - */ -internal class JsonFieldPatternBuilder : JsonFieldPatternBuilder() { - override fun buildReplacement( - visibleCharacters: Int, - vararg fieldNames: String?, - ): String = "\"$1$2$OMITTED\"" -} diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/LogMasker.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/LogMasker.kt new file mode 100644 index 00000000..e69cced3 --- /dev/null +++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/LogMasker.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.sdk.core.logging.masking + +import com.ebay.ejmask.api.MaskingPattern + +internal class LogMasker( + globalMaskedFields: Set = emptySet(), + pathMaskedFields: Set> = emptySet(), +) : (String) -> String { + private val patterns: List = + MaskingPatternBuilder() + .apply { + globalFields(globalMaskedFields) + pathFields(pathMaskedFields) + }.build() + + /** + * Applies all masking patterns to the input string. + * + * @param input The input string to be masked. + * @return The masked string. + */ + override fun invoke(input: String): String { + var masked = input + + patterns.forEach { pattern: MaskingPattern -> + masked = pattern.replaceAll(masked) + } + + return masked + } +} diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskLogsUtils.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskLogsUtils.kt deleted file mode 100644 index 9bcadaaa..00000000 --- a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskLogsUtils.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.expediagroup.sdk.core.logging.masking - -import com.ebay.ejmask.core.BaseFilter -import com.ebay.ejmask.core.EJMask -import com.ebay.ejmask.core.EJMaskInitializer -import com.ebay.ejmask.core.util.LoggerUtil - -/** - * Masks sensitive information within the provided log string. - * - * @param logs The log string that may contain sensitive information requiring masking. - * @return A new log string with sensitive information masked. - */ -fun maskLogs(logs: String): String = MaskLogs.execute(logs) - -/** - * Configures log masking by adding specified fields to the mask list. - * - * This function integrates with the log masking system to include additional fields - * that should be masked in the logs. The fields provided in the parameter are added - * to the set of fields that will have their values masked when logs are generated. - * - * @param fields The set of field names that need to be masked in the logs. - */ -fun configureLogMasking(fields: Set) { - MaskLogs.addFields(fields) -} - -/** - * Checks if a specified field is among the fields that should be masked. - * - * @param field The name of the field to check. - * @return `true` if the field should be masked, `false` otherwise. - */ -fun isMaskedField(field: String): Boolean = MaskLogs.maskedFields.contains(field) - -/** - * A utility class for masking sensitive information in log strings. - * - * The `MaskLogs` class is designed to replace sensitive information within logs with masked values. - * The class implements the `Function1` interface, enabling it to be invoked with a log string - * to produce a masked version of the string. - * - * The masking process relies on predefined filters that determine which fields within the log - * should be masked. Filters can be added and configured using the companion object's methods. - */ -private class MaskLogs : (String) -> String { - companion object { - @JvmStatic - val filters: MutableList = mutableListOf() - - val maskedFields: MutableSet = mutableSetOf() - - @JvmStatic - val INSTANCE = MaskLogs() - - /** - * Executes the masking process on the provided log string. - * - * @param logs The log string that may contain sensitive information requiring masking. - */ - @JvmStatic - fun execute(logs: String) = INSTANCE(logs) - - /** - * Adds specified fields to the list of fields to be masked in logs. - * - * The fields provided in the parameter are added to the internal set of fields - * and corresponding filters are created and added to the filter list. These filters - * are then integrated into the masking system to ensure the specified fields are - * masked in any logs they appear in. - * - * @param fields The set of field names that need to be masked in the logs. - */ - @JvmStatic - fun addFields(fields: Set) { - maskedFields.addAll(fields) - filters.add(JsonFieldFilter(fields.toTypedArray())) - filters.forEach { EJMaskInitializer.addFilter(it) } - } - } - - init { - LoggerUtil.register { _, _, _ -> - // disable logging - } - } - - /** - * Masks the given text using the EJMask utility. - * - * @param text The input text that needs to be masked. - * @return The masked version of the input text. - */ - override fun invoke(text: String): String = EJMask.mask(text) -} diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskingPatternBuilder.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskingPatternBuilder.kt new file mode 100644 index 00000000..6a0c7dd2 --- /dev/null +++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/MaskingPatternBuilder.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.sdk.core.logging.masking + +import com.ebay.ejmask.api.MaskingPattern + +/** + * Builder class for creating masking patterns. + */ +internal class MaskingPatternBuilder { + private var globalFields: Set = setOf() + private var pathFields: Set> = setOf() + + /** + * Adds global fields to be masked. + * + * @param fields Vararg of field names to be masked globally. + * @return The current instance of MaskingPatternBuilder. + */ + fun globalFields(fields: Set): MaskingPatternBuilder = + apply { + globalFields += fields.toSortedSet() + } + + /** + * Adds path-specific fields to be masked. + * This method updates the `globalFields` set by adding the provided field names. + * + * Takes the last two elements of each list to be used as the path and ignores the rest. Temporary solution + * until ejmask add support for unlimited-fields path-based masking. + * + * @param paths Vararg of lists of field names to be masked by path. + * @return The current instance of MaskingPatternBuilder. + */ + fun pathFields(paths: Set>) = + apply { + pathFields += paths.map { it.takeLast(2) } + } + + /** + * Builds the list of MaskingPattern based on the added global and path fields. + * + * @return A list of MaskingPattern. + */ + fun build(): List = + buildList { + /** + * Builds masking patterns for global fields. + * + * @return A list of MaskingPattern for global fields. + */ + fun buildGlobalFieldsMaskingPattern(): List = + if (globalFields.isEmpty()) { + emptyList() + } else { + val patternGenerator = CustomJsonFullValuePatternBuilder() + listOf( + MaskingPattern( + 0, + patternGenerator.buildPattern(0, *globalFields.toTypedArray()), + patternGenerator.buildReplacement(0, *globalFields.toTypedArray()), + ), + ) + } + + /** + * Builds masking patterns for path-specific fields. + * + * @return A list of MaskingPattern for path-specific fields. + */ + fun buildPathFieldsMaskingPattern(): List = + buildList { + pathFields.forEachIndexed { index, path -> + CustomJsonRelativeFieldPatternBuilder().also { patternGenerator -> + add( + MaskingPattern( + index + 1, + patternGenerator.buildPattern(1, *path.toTypedArray()), + patternGenerator.buildReplacement(1, *path.toTypedArray()), + ), + ) + } + } + } + + addAll(buildGlobalFieldsMaskingPattern()) + addAll(buildPathFieldsMaskingPattern()) + } +} diff --git a/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/PatternBuilders.kt b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/PatternBuilders.kt new file mode 100644 index 00000000..de4da35a --- /dev/null +++ b/code/src/main/kotlin/com/expediagroup/sdk/core/logging/masking/PatternBuilders.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.sdk.core.logging.masking + +import com.ebay.ejmask.extenstion.builder.json.JsonFullValuePatternBuilder +import com.ebay.ejmask.extenstion.builder.json.JsonRelativeFieldPatternBuilder +import com.expediagroup.sdk.core.logging.common.Constant.OMITTED + +/** + * Custom implementation of JsonFieldPatternBuilder for building JSON field patterns. + */ +internal class CustomJsonFullValuePatternBuilder : JsonFullValuePatternBuilder() { + companion object { + @JvmStatic + val REPLACEMENT_TEMPLATE = "\"$1$2$OMITTED\"" + } + + /** + * Builds the replacement string for the given field names. + * Ignores the visibleCharacters parameter. + * + * @param visibleCharacters Number of visible characters. Value is ignored. + * @param fieldNames Vararg of field names. + * @return The replacement string. + */ + override fun buildReplacement( + visibleCharacters: Int, + vararg fieldNames: String?, + ): String = REPLACEMENT_TEMPLATE +} + +internal class CustomJsonRelativeFieldPatternBuilder : JsonRelativeFieldPatternBuilder() { + companion object { + @JvmStatic + val REPLACEMENT_TEMPLATE = "$1$OMITTED$3" + } + + /** + * Builds the replacement string for the given field names. + * Ignores the visibleCharacters parameter. + * + * @param visibleCharacters Number of visible characters. Value is ignored. + * @param fieldNames Vararg of field names. + * @return The replacement string. + */ + override fun buildReplacement( + visibleCharacters: Int, + vararg fieldNames: String?, + ): String = REPLACEMENT_TEMPLATE +} diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/common/RequestExecutor.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/common/RequestExecutor.kt index f77e1393..ceaf07c8 100644 --- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/common/RequestExecutor.kt +++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/common/RequestExecutor.kt @@ -8,6 +8,7 @@ import com.expediagroup.sdk.core.client.Transport import com.expediagroup.sdk.core.common.RequestHeadersInterceptor import com.expediagroup.sdk.core.interceptor.Interceptor import com.expediagroup.sdk.core.logging.LoggingInterceptor +import com.expediagroup.sdk.core.logging.common.LoggerDecorator import com.expediagroup.sdk.core.okhttp.BaseOkHttpClient import com.expediagroup.sdk.core.okhttp.OkHttpTransport import com.expediagroup.sdk.lodgingconnectivity.configuration.ApiEndpoint @@ -24,11 +25,12 @@ internal fun getHttpTransport(configuration: ClientConfiguration): Transport = class RequestExecutor( configuration: ClientConfiguration, apiEndpoint: ApiEndpoint, + logger: LoggerDecorator, ) : AbstractRequestExecutor(getHttpTransport(configuration)) { override val interceptors: List = listOf( RequestHeadersInterceptor(), - LoggingInterceptor(), + LoggingInterceptor(logger), BearerAuthenticationInterceptor( BearerAuthenticationManager( requestExecutor = this, diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/payment/PaymentClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/payment/PaymentClient.kt index f0c6f3c1..42375d2f 100644 --- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/payment/PaymentClient.kt +++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/payment/PaymentClient.kt @@ -16,6 +16,8 @@ package com.expediagroup.sdk.lodgingconnectivity.payment +import com.expediagroup.sdk.core.logging.common.LoggerDecorator +import com.expediagroup.sdk.core.logging.masking.LogMasker import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupServiceException import com.expediagroup.sdk.graphql.common.AbstractGraphQLExecutor import com.expediagroup.sdk.graphql.common.GraphQLExecutor @@ -27,6 +29,7 @@ import com.expediagroup.sdk.lodgingconnectivity.configuration.EndpointProvider import com.expediagroup.sdk.lodgingconnectivity.payment.operation.GetPaymentInstrumentResponse import com.expediagroup.sdk.lodgingconnectivity.payment.operation.PaymentInstrumentQuery import com.expediagroup.sdk.lodgingconnectivity.payment.operation.getPaymentInstrumentOperation +import org.slf4j.LoggerFactory /** * A client for interacting with EG Lodging Connectivity Payment PCI GraphQL API. @@ -44,7 +47,12 @@ class PaymentClient( override val graphQLExecutor: AbstractGraphQLExecutor = GraphQLExecutor( - requestExecutor = RequestExecutor(config, apiEndpoint), + requestExecutor = + RequestExecutor( + config, + apiEndpoint, + logger, + ), serverUrl = apiEndpoint.endpoint, ) @@ -63,4 +71,16 @@ class PaymentClient( run { getPaymentInstrumentOperation(graphQLExecutor, token) } + + companion object { + @JvmStatic + private val logger = + LoggerDecorator( + logger = LoggerFactory.getLogger(PaymentClient::class.java.enclosingClass), + masker = + LogMasker( + globalMaskedFields = setOf("cvv", "cvv2"), + ), + ) + } } diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/sandbox/SandboxDataManagementClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/sandbox/SandboxDataManagementClient.kt index 7855087d..4bac760e 100644 --- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/sandbox/SandboxDataManagementClient.kt +++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/sandbox/SandboxDataManagementClient.kt @@ -16,6 +16,8 @@ package com.expediagroup.sdk.lodgingconnectivity.sandbox +import com.expediagroup.sdk.core.logging.common.LoggerDecorator +import com.expediagroup.sdk.core.logging.masking.LogMasker import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupServiceException import com.expediagroup.sdk.graphql.common.AbstractGraphQLExecutor import com.expediagroup.sdk.graphql.common.GraphQLExecutor @@ -24,6 +26,7 @@ import com.expediagroup.sdk.lodgingconnectivity.common.RequestExecutor import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientConfiguration import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientEnvironment import com.expediagroup.sdk.lodgingconnectivity.configuration.EndpointProvider +import com.expediagroup.sdk.lodgingconnectivity.payment.PaymentClient import com.expediagroup.sdk.lodgingconnectivity.sandbox.operation.type.CancelReservationInput import com.expediagroup.sdk.lodgingconnectivity.sandbox.operation.type.ChangeReservationStayDatesInput import com.expediagroup.sdk.lodgingconnectivity.sandbox.operation.type.CreatePropertyInput @@ -61,6 +64,7 @@ import com.expediagroup.sdk.lodgingconnectivity.sandbox.reservation.operation.ge import com.expediagroup.sdk.lodgingconnectivity.sandbox.reservation.operation.getSandboxReservationsOperation import com.expediagroup.sdk.lodgingconnectivity.sandbox.reservation.operation.updateSandboxReservationOperation import com.expediagroup.sdk.lodgingconnectivity.sandbox.reservation.paginator.SandboxReservationsPaginator +import org.slf4j.LoggerFactory /** * A client for interacting with EG Lodging Connectivity Sandbox GraphQL API. @@ -81,7 +85,7 @@ class SandboxDataManagementClient( override val graphQLExecutor: AbstractGraphQLExecutor = GraphQLExecutor( - requestExecutor = RequestExecutor(config, apiEndpoint), + requestExecutor = RequestExecutor(config, apiEndpoint, logger), serverUrl = apiEndpoint.endpoint, ) @@ -335,4 +339,16 @@ class SandboxDataManagementClient( run { deleteSandboxReservationsOperation(graphQLExecutor, input) } + + companion object { + @JvmStatic + private val logger = + LoggerDecorator( + logger = LoggerFactory.getLogger(PaymentClient::class.java.enclosingClass), + masker = + LogMasker( + globalMaskedFields = setOf("cvv", "cvv2"), + ), + ) + } } diff --git a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/supply/reservation/ReservationClient.kt b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/supply/reservation/ReservationClient.kt index 20965d95..dd915791 100644 --- a/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/supply/reservation/ReservationClient.kt +++ b/code/src/main/kotlin/com/expediagroup/sdk/lodgingconnectivity/supply/reservation/ReservationClient.kt @@ -16,6 +16,8 @@ package com.expediagroup.sdk.lodgingconnectivity.supply.reservation +import com.expediagroup.sdk.core.logging.common.LoggerDecorator +import com.expediagroup.sdk.core.logging.masking.LogMasker import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupServiceException import com.expediagroup.sdk.graphql.common.AbstractGraphQLExecutor import com.expediagroup.sdk.graphql.common.GraphQLExecutor @@ -24,6 +26,7 @@ import com.expediagroup.sdk.lodgingconnectivity.common.RequestExecutor import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientConfiguration import com.expediagroup.sdk.lodgingconnectivity.configuration.ClientEnvironment import com.expediagroup.sdk.lodgingconnectivity.configuration.EndpointProvider +import com.expediagroup.sdk.lodgingconnectivity.payment.PaymentClient import com.expediagroup.sdk.lodgingconnectivity.supply.operation.type.CancelReservationInput import com.expediagroup.sdk.lodgingconnectivity.supply.operation.type.CancelReservationReconciliationInput import com.expediagroup.sdk.lodgingconnectivity.supply.operation.type.CancelVrboReservationInput @@ -46,6 +49,7 @@ import com.expediagroup.sdk.lodgingconnectivity.supply.reservation.operation.con import com.expediagroup.sdk.lodgingconnectivity.supply.reservation.operation.refundReservationOperation import com.expediagroup.sdk.lodgingconnectivity.supply.reservation.paginator.ReservationsPaginator import com.expediagroup.sdk.lodgingconnectivity.supply.reservation.stream.ReservationsStream +import org.slf4j.LoggerFactory /** * A client for interacting with EG Lodging Connectivity Reservations GraphQL API @@ -66,7 +70,7 @@ class ReservationClient( override val graphQLExecutor: AbstractGraphQLExecutor = GraphQLExecutor( - requestExecutor = RequestExecutor(config, apiEndpoint), + requestExecutor = RequestExecutor(config, apiEndpoint, logger), serverUrl = apiEndpoint.endpoint, ) @@ -291,4 +295,16 @@ class ReservationClient( run { refundReservationOperation(graphQLExecutor, input, selections) } + + companion object { + @JvmStatic + private val logger = + LoggerDecorator( + logger = LoggerFactory.getLogger(PaymentClient::class.java.enclosingClass), + masker = + LogMasker( + globalMaskedFields = setOf("cvv", "cvv2"), + ), + ) + } } diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/LogMaskerTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/LogMaskerTest.kt new file mode 100644 index 00000000..c426c42a --- /dev/null +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/LogMaskerTest.kt @@ -0,0 +1,192 @@ +package com.expediagroup.sdk.core.logging.masking + +import com.expediagroup.sdk.core.logging.common.Constant.OMITTED +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class LogMaskerTest { + @Nested + inner class FieldMaskingConfigurationTest { + @Test + fun `adding global fields to the masking pattern builder`() { + val globalFields = setOf("globalField1", "globalField2") + val patterns = MaskingPatternBuilder().globalFields(globalFields).build() + + assertEquals(1, patterns.size) + + assertTrue( + patterns + .first() + .pattern + .pattern() + .contains("globalField1"), + ) + assertTrue( + patterns + .first() + .pattern + .pattern() + .contains("globalField2"), + ) + } + + @Test + fun `adding path fields to the masking pattern builder`() { + val pathFields = setOf(listOf("first", "second"), listOf("first1", "second1", "third1", "as")) + val patterns = MaskingPatternBuilder().pathFields(pathFields).build() + + assertEquals(pathFields.size, patterns.size) + } + + @Test + fun `adding global and path fields to the masking pattern builder`() { + val globalFields = setOf("globalField1", "globalField2") + val pathFields = setOf(listOf("first", "second"), listOf("first1", "second1", "third1", "as")) + + val maskingPatterns = + MaskingPatternBuilder() + .globalFields(globalFields) + .pathFields(pathFields) + .build() + + // 1 global field pattern + 2 path field patterns + assertEquals(pathFields.size + 1, maskingPatterns.size) + } + } + + @Nested + inner class MaskingBehaviorTest { + @Test + fun `masks global fields in json payload`() { + val globalFields = setOf("globalField1", "globalField2") + val patterns = MaskingPatternBuilder().globalFields(globalFields).build() + + assertEquals(1, patterns.size) + + val actual = + patterns.first().replaceAll( + """ + { + "globalField1": "value1", + "globalField2": { + "globalField1": "value2" + } + } + """.trimIndent(), + ) + val expected = + """ + { + "globalField1": "$OMITTED", + "globalField2": { + "globalField1": "$OMITTED" + } + } + """.trimIndent() + + assertEquals(expected, actual) + } + + @Test + fun `masks path fields in json payload`() { + val pathFields = setOf(listOf("first", "second"), listOf("first1", "second1", "third1", "as")) + val patterns = MaskingPatternBuilder().pathFields(pathFields).build() + + assertEquals(pathFields.size, patterns.size) + + var actual = + """ + { + "first": { + "second": "value1" + }, + "first1": { + "second1": { + "third1": { + "as": "value2" + } + } + } + } + """.trimIndent() + patterns.forEach { actual = it.replaceAll(actual) } + + val expected = + """ + { + "first": { + "second": "$OMITTED" + }, + "first1": { + "second1": { + "third1": { + "as": "$OMITTED" + } + } + } + } + """.trimIndent() + + assertEquals(expected, actual) + } + + @Test + fun `masks global and path fields in json payload`() { + val globalFields = setOf("globalField1", "globalField2") + val pathFields = setOf(listOf("first", "second"), listOf("first1", "second1", "third1", "as")) + + val maskingPatterns = + MaskingPatternBuilder() + .globalFields(globalFields) + .pathFields(pathFields) + .build() + + assertEquals(pathFields.size + 1, maskingPatterns.size) + + var actual = + """ + { + "globalField1": "value1", + "globalField2": { + "globalField1": "value2" + }, + "first": { + "second": "value1" + }, + "first1": { + "second1": { + "third1": { + "as": "value2" + } + } + } + } + """.trimIndent() + maskingPatterns.forEach { actual = it.replaceAll(actual) } + + val expected = + """ + { + "globalField1": "$OMITTED", + "globalField2": { + "globalField1": "$OMITTED" + }, + "first": { + "second": "$OMITTED" + }, + "first1": { + "second1": { + "third1": { + "as": "$OMITTED" + } + } + } + } + """.trimIndent() + + assertEquals(expected, actual) + } + } +} diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/MaskingPatternBuilderTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/MaskingPatternBuilderTest.kt new file mode 100644 index 00000000..0a622335 --- /dev/null +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/MaskingPatternBuilderTest.kt @@ -0,0 +1,269 @@ +package com.expediagroup.sdk.core.logging.masking + +import com.expediagroup.sdk.core.logging.common.Constant.OMITTED +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll + +class MaskingPatternBuilderTest { + @Nested + inner class PatternGenerationTest { + @Test + fun `generated masking patterns are empty if no fields are passed`() { + assertEquals(0, MaskingPatternBuilder().build().size) + } + + @Test + fun `generated masking patterns size is one when only global fields are passed regardless of their count`() { + val globalFields = setOf("globalField1", "globalField2") + val patterns = MaskingPatternBuilder().globalFields(globalFields).build() + + assertEquals(1, patterns.size) + } + + @Test + fun `passed path fields are truncated to the last two elements`() { + val pathFields = setOf(listOf("first", "second", "third", "fourth")) + val patterns = MaskingPatternBuilder().pathFields(pathFields).build() + + assertEquals(1, patterns.size) + + assertFalse( + patterns + .first() + .pattern + .pattern() + .contains("first"), + ) + assertFalse( + patterns + .first() + .pattern + .pattern() + .contains("second"), + ) + + assertTrue( + patterns + .first() + .pattern + .pattern() + .contains("third"), + ) + assertTrue( + patterns + .first() + .pattern + .pattern() + .contains("fourth"), + ) + } + + @Test + fun `generated masking patterns size is not affected by pattern duplication caused by truncated patterns`() { + val pathFields = setOf(listOf("second", "third"), listOf("first", "second", "third")) + val patterns = MaskingPatternBuilder().pathFields(pathFields).build() + + assertEquals(1, patterns.size) + } + + @Test + fun `generated masking patterns size grows linearly when only path-based fields are passed`() { + val pathFields = setOf(listOf("first", "second"), listOf("first", "second", "third")) + val patterns = MaskingPatternBuilder().pathFields(pathFields).build() + + assertEquals(pathFields.size, patterns.size) + } + + @Test + fun `generated masking patterns size equals the number of path-based fields passed +1 for global fields when both are passed`() { + val globalFields = setOf("globalField1", "globalField2") + val pathFields = setOf(listOf("first", "second"), listOf("first", "second", "third")) + + val maskingPatterns = + MaskingPatternBuilder() + .globalFields(globalFields) + .pathFields(pathFields) + .build() + + assertEquals(pathFields.size + 1, maskingPatterns.size) + } + + @Test + fun `global path fields are passed to builder and consumed in masking patterns generation`() { + val globalFields = setOf("globalField1", "globalField2") + val patterns = MaskingPatternBuilder().globalFields(globalFields).build() + + assertEquals(1, patterns.size) + + assertTrue( + patterns + .first() + .pattern + .pattern() + .contains("globalField1"), + ) + assertTrue( + patterns + .first() + .pattern + .pattern() + .contains("globalField2"), + ) + } + + @Test + fun `path fields are passed to builder and consumed in masking patterns generation`() { + val pathFields = setOf(listOf("first", "second"), listOf("first1", "second1", "third1", "as")) + val patterns = MaskingPatternBuilder().pathFields(pathFields).build() + + assertEquals(pathFields.size, patterns.size) + + assertAll( + { assertTrue(patterns[0].pattern.pattern().contains("first")) }, + { assertTrue(patterns[0].pattern.pattern().contains("second")) }, + { assertTrue(patterns[1].pattern.pattern().contains("third1")) }, + { assertTrue(patterns[1].pattern.pattern().contains("as")) }, + ) + } + } + + @Nested + inner class PatternsBehaviourTest { + @Test + fun `masks a field globally when a global field is passed`() { + val globalFields = setOf("globalField1", "globalField2") + val patterns = MaskingPatternBuilder().globalFields(globalFields).build() + + assertEquals(1, patterns.size) + + val actual = + patterns.first().replaceAll( + """ + { + "globalField1": "value1", + "globalField2": { + "globalField1": "value2" + } + } + """.trimIndent(), + ) + val expected = + """ + { + "globalField1": "$OMITTED", + "globalField2": { + "globalField1": "$OMITTED" + } + } + """.trimIndent() + + assertEquals(expected, actual) + } + + @Test + fun `masks a field in a path when a path field is passed`() { + val pathFields = setOf(listOf("first", "second"), listOf("first1", "second1", "third1", "as")) + val patterns = MaskingPatternBuilder().pathFields(pathFields).build() + + assertEquals(pathFields.size, patterns.size) + + var actual = + """ + { + "first": { + "second": "value1" + }, + "first1": { + "second1": { + "third1": { + "as": "value2" + } + } + } + } + """.trimIndent() + + patterns.forEach { actual = it.replaceAll(actual) } + + val expected = + """ + { + "first": { + "second": "$OMITTED" + }, + "first1": { + "second1": { + "third1": { + "as": "$OMITTED" + } + } + } + } + """.trimIndent() + + assertEquals(expected, actual) + } + + @Test + fun `masks a field globally and in a path when both are passed`() { + val globalFields = setOf("globalField1", "globalField2") + val pathFields = setOf(listOf("first", "second"), listOf("first1", "second1", "third1", "as")) + + val maskingPatterns = + MaskingPatternBuilder() + .globalFields(globalFields) + .pathFields(pathFields) + .build() + + assertEquals(pathFields.size + 1, maskingPatterns.size) + + var actual = + """ + { + "globalField1": "value1", + "globalField2": { + "globalField1": "value2" + }, + "first": { + "second": "value3" + }, + "first1": { + "second1": { + "third1": { + "as": "value4" + } + } + } + } + """.trimIndent() + + maskingPatterns.forEach { actual = it.replaceAll(actual) } + + val expected = + """ + { + "globalField1": "$OMITTED", + "globalField2": { + "globalField1": "$OMITTED" + }, + "first": { + "second": "$OMITTED" + }, + "first1": { + "second1": { + "third1": { + "as": "$OMITTED" + } + } + } + } + """.trimIndent() + + assertEquals(expected, actual) + } + } +} diff --git a/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/PatternBuildersTest.kt b/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/PatternBuildersTest.kt new file mode 100644 index 00000000..f2b2cf37 --- /dev/null +++ b/code/src/test/kotlin/com/expediagroup/sdk/core/logging/masking/PatternBuildersTest.kt @@ -0,0 +1,65 @@ +package com.expediagroup.sdk.core.logging.masking + +import com.expediagroup.sdk.core.logging.common.Constant.OMITTED +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PatternBuildersTest { + @Nested + inner class CustomJsonFullValuePatternBuilderTest { + @Test + fun `buildReplacement contains omitted keyword`() { + val custom = CustomJsonFullValuePatternBuilder() + + assertTrue(custom.buildReplacement(0, "field").contains(OMITTED)) + } + + @Test + fun `buildReplacement always generates proper replacement template`() { + val custom = CustomJsonFullValuePatternBuilder() + + custom.buildReplacement(0, "field1").also { + assertEquals(CustomJsonFullValuePatternBuilder.REPLACEMENT_TEMPLATE, it) + } + + custom.buildReplacement(0, "field1", "field2").also { + assertEquals(CustomJsonFullValuePatternBuilder.REPLACEMENT_TEMPLATE, it) + } + + custom.buildReplacement(0, "field".repeat(1000)).also { + assertEquals(CustomJsonFullValuePatternBuilder.REPLACEMENT_TEMPLATE, it) + } + } + } + + @Nested + inner class CustomJsonRelativeFieldPatternBuilderTest { + @Test + fun `buildReplacement contains omitted keyword`() { + val custom = CustomJsonRelativeFieldPatternBuilder() + + assertTrue(custom.buildReplacement(0, "field").contains(OMITTED)) + } + + @Test + fun `buildReplacement always generates proper replacement template`() { + val custom = CustomJsonRelativeFieldPatternBuilder() + + custom.buildReplacement(0, "field1").also { + assertEquals(CustomJsonRelativeFieldPatternBuilder.REPLACEMENT_TEMPLATE, it) + } + + custom.buildReplacement(0, "field1", "field2").also { + assertEquals(CustomJsonRelativeFieldPatternBuilder.REPLACEMENT_TEMPLATE, it) + } + + custom.buildReplacement(0, "field".repeat(1000)).also { + assertEquals(CustomJsonRelativeFieldPatternBuilder.REPLACEMENT_TEMPLATE, it) + } + } + } +}