Code Monkey home page Code Monkey logo

shadow-tool's Introduction

Shadow Tool

This library allows you to safely test your migration from one back-end to another in production!

The Shadow Tool can be easily integrated into your Java/Kotlin project and allows you to compare the current back-end service your application is using against the new back-end you plan on using. Since it actually runs in the production environment (in the background), it helps to ensure that:

  1. the connection towards your new back-end is working and gives you a response,
  2. the data coming from the new back-end is equal to the data coming from the current back-end,
  3. whether your code correctly maps the data of the new back-end to your existing domain.

The tool is designed to be a plug-and-play solution that runs without impacting the functionality of your current production app. When activated, as your app fetches data from your current back-end, it will also call the new back-end and compare the data in parallel. This will be sampled based on a configured percentage to prevent overloading your application. The findings are reported using log statements.

Installation

Maven

Maven Central

<dependency>
    <groupId>io.github.rabobank</groupId>
    <artifactId>shadow-tool</artifactId>
    <version>${shadow-tool.version}</version> <!-- Make sure to check the latest version in Maven Central (above) -->
</dependency>

Gradle

implementation("io.github.rabobank:shadow-tool:$version") // Make sure to check the latest version in Maven Central (above)

Getting started

  1. Important: In order to see the differences in your logs, you have to add slf4j-api to your dependencies. By default, only fieldnames (keys) are logged when the values differ. To see the what exactly is different, encryption is required. Proceed to step 2 for setting up encryption.
  2. You have 3 encryption options:
    1. Noop encryption: By setting up a NoopEncryptionService, the differences are logged as Base64 encoded text. This is not recommended for sensitive data.
      Example:
      import io.github.rabobank.shadow_tool.ShadowFlow.ShadowFlowBuilder;
      
      import java.util.List;
      import java.util.function.Supplier;
      
      public class BackendService {
      
          public DummyObject callBackend() {
              // Create a ShadowFlow instance with NoopEncryptionService
              // The 10 means that for 10% of all requests, the `newBackend` is invoked as well and its response is compared against the `currentBackend` response.
              ShadowFlowBuilder<Dummy> builder = new ShadowFlowBuilder<>(10);
              ShadowFlow<Dummy> shadowFlow = builder.withEncryptionService(NoopEncryptionService.INSTANCE).build();
      
              // Define your current backend and new backend suppliers
              Supplier<Dummy> currentBackend = () -> {
                  // Your current backend logic here
                  return new Dummy("Bob", "Utrecht", List.of("Mirabel", "Bruno"));
              };
      
              Supplier<Dummy> newBackend = () -> {
                  // Your new backend logic here
                  return new Dummy("Bob", "Amsterdam", List.of("Bruno", "Mirabel", "Mirabel"));
              };
      
              // The result is always from the first supplier. So in this case, the return value always yields the response of the `currentBackend` service.
              return shadowFlow.compare(currentBackend, newBackend);
          }
      }
    2. Cipher encryption: The differences are logged as encrypted values. This is recommended for sensitive data. Example:
      import io.github.rabobank.shadow_tool.ShadowFlow.ShadowFlowBuilder;
      
      import javax.crypto.Cipher;
      import java.security.GeneralSecurityException;
      import java.util.List;
      import java.util.function.Supplier;
      
      public class BackendService {
      
          public DummyObject callBackend() {
              // Create a Cipher instance
              Cipher cipher = null;
              try {
                  // The AES key (16, 24, or 32 bytes)
                  final var keyBytes = Hex.decodeStrict("3d7e0c4f8fbbd8d8a79e76cabc8f4e24");
                  final var secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
          
                  // Initialization Vector (IV) for GCM
                  final var iv = Hex.decodeStrict("3d7e0c4f8fbb"); // 96 bits IV
                  if (iv.length != GCM_SIV_IV_SIZE) {
                      throw new IllegalArgumentException("Initialization Vector should be 12 bytes / 96 bits");
                  }
          
                  // Create AEADParameterSpec
                  final var gcmParameterSpec = new GCMParameterSpec(MAC_SIZE_IN_BITS, iv);
                  // Create Cipher instance with the specified algorithm and provider
                  cipher = Cipher.getInstance(ALGORITHM_MODE);
          
                  // Initialize the Cipher for encryption or decryption
                  cipher.init(ENCRYPT_MODE, secretKey, gcmParameterSpec);
              } catch (GeneralSecurityException e) {
                  // Handle exception
              }
      
              // Create a ShadowFlow instance with DefaultEncryptionService
              // The 10 means that for 10% of all requests, the `newBackend` is invoked as well and its response is compared against the `currentBackend` response.
              ShadowFlow<Dummy> shadowFlow = new ShadowFlowBuilder<Dummy>(10).withCipher(cipher).build();
      
              // Define your current backend and new backend suppliers
              Supplier<Dummy> currentBackend = () -> {
                  // Your current backend logic here
                  return new Dummy("Bob", "Utrecht", List.of("Mirabel", "Bruno"));
              };
      
              Supplier<Dummy> newBackend = () -> {
                  // Your new backend logic here
                  return new Dummy("Bob", "Amsterdam", List.of("Bruno", "Mirabel", "Mirabel"));
              };
      
              // The result is always from the first supplier. So in this case, the return value always yields the response of the `currentBackend` service.
              return shadowFlow.compare(currentBackend, newBackend);
          }
      }
    3. PublicKey encryption: The differences are logged as encrypted values. This is recommended for sensitive data. Example:
      import io.github.rabobank.shadow_tool.ShadowFlow.ShadowFlowBuilder;
      
      import java.security.KeyFactory;
      import java.security.PublicKey;
      import java.security.spec.X509EncodedKeySpec;
      import java.util.Base64;
      import java.util.List;
      import java.util.function.Supplier;
      
      public class BackendService {
      
         public DummyObject callBackend() {
             final PublicKey publicKey;
             try {
                 publicKey = KeyFactory.getInstance("RSA")
                         .generatePublic(new X509EncodedKeySpec(Base64.decode("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArmkP2CgDn3OsuIj1GxM3")));
             } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
                 throw new RuntimeException(e);
             }
      
             // Create a ShadowFlow instance with PublicKeyEncryptionService
             // The 10 means that for 10% of all requests, the `newBackend` is invoked as well and its response is compared against the `currentBackend` response.
             ShadowFlowBuilder<Dummy> builder = new ShadowFlowBuilder<>(10);
             builder.withEncryption(publicKey);
      
             ShadowFlow<Dummy> shadowFlow = builder.build();
      
             // Define your current backend and new backend suppliers
             Supplier<Dummy> currentBackend = () -> {
                 // Your current backend logic here
                 return new Dummy("Bob", "Utrecht", List.of("Mirabel", "Bruno"));
             };
      
             Supplier<Dummy> newBackend = () -> {
                 // Your new backend logic here
                 return new Dummy("Bob", "Amsterdam", List.of("Bruno", "Mirabel", "Mirabel"));
             };
             // The result is always from the first supplier. So in this case, the return value always yields the response of the `currentBackend` service.
             return shadowFlow.compare(currentBackend, newBackend);
                }
         }       
  3. To create a public and private (to decrypt) key, run the following command:
    openssl genrsa -out pair.pem 2048 && openssl rsa -in pair.pem -pubout -out public.key && openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pair.pem -out private.key && rm -rf pair.pem

The shadow tool invokes both services asynchronously, so it will not have impact on the main flow performance-wise. Be aware that the more often the Shadow Tool runs, the more resources your application uses and back-ends are called. Be careful not to set this number too high for high-traffic applications.

For a fair comparison, both services are required to return the same domain classes. In the example above, we called it Dummy. Also, since the secondary call is already mapped to the correct domain, completing the migration is straightforward: simply replace the first call with the secondary call and remove the Shadow Tool code.

You can distinguish the results of multiple shadow flows running in your application by setting an instance name. This will be part of the log messages.

Reactive

The Shadow Tool also provides a reactive API based on Project Reactor.

class MyService {
    // fields and constructor

    public Mono<Dummy> getDummy() {
        return shadowFlow.compare(
                getDummyFromCurrent(),
                getDummyFromNew()
        );
    }

    private Mono<Dummy> getDummyFromCurrent() {
        return yourCurrentBackend.getDummMono()
                .map(...);
    }

    private Mono<AccountInfo> getDummyFromNew() {
        return yourNewBackend.getDummyMono()
                .map(...);
    }
} 

Logs

The Shadow Tool logs any differences it finds between the two flows. It always logs the field names of the objects containing the differences, and it can also log the values when encryption is set up. You can expect output similar to the following:

# Without Encryption enabled
The following differences were found: firstName, lastName

# With Encryption enabled
The following differences were found: firstName, lastName. Encrypted values: 6U8H2WSpEoXY1cFDS2Ze/63ohRVIS4t3A4I5E3RJeemrqXTWEUN6BlTawMVgyjQri9t8l6t9jotJmIEQOoc++C9W38Z8mYEAzU2UzvGm50AMcFqEXheSBEw7c3LZFRoE

Inspecting the values of differences

Values are encrypted using the public key that is set up during the configuration. The default algorithm for Public Key encryption is RSA with Electronic Codeblock mode (CBC) and OAEPWITHSHA-256ANDMGF1PADDING padding.

Example of decrypting values of differences

Here's an example decrypting the encrypted values using a Cipher (key and iv):

$ encrypted_text="6U8H2WSpEoXY1cFDS2Ze/63ohRVIS4t3A4I5E3RJeemrqXTWEUN6BlTawMVgyjQri9t8l6t9jotJmIEQOoc++C9W38Z8mYEAzU2UzvGm50AMcFqEXheSBEw7c3LZFRoE"
$ key="2d4a75512e73b8761400b49aff747af368a18de82d3865fe597efaf6d11053f9"
$ iv="ebc3a59998fe444066b5fd819578d564"
$ echo -n $encrypted_text | openssl enc -d -aes-256-cbc -base64 -nosalt -A -K $key -iv $iv
'firstName' changed: 'terry' -> 'Terry'
'lastName' changed: 'pratchett' -> 'Pratchett'

Or you can find an example in one of the tests: EncryptionServiceTest.

shadow-tool's People

Contributors

dependabot[bot] avatar erwinc1 avatar martinvisser avatar renovate-bot avatar mdpadberg avatar martinkanters avatar stevenschenk avatar nielsdt-rabobank avatar

Stargazers

Adam Michalik avatar  avatar BitWalls-ops avatar Ashwin Jayaprakash avatar Willem Dekker avatar  avatar  avatar  avatar Hilario avatar Michèl Breevoort avatar João Paulo Gomes avatar  avatar Tyler Van Gorder avatar

Watchers

Egbert avatar Hilario avatar  avatar  avatar  avatar

Forkers

martinkanters

shadow-tool's Issues

Implementing EncryptionService is not possible

Describe the bug
The interface isn't made public so it's therefore impossible to implement to be used for custom encryption.

To Reproduce
Steps to reproduce the behavior:

  1. Implement the interface outside of the Shadow Tool
  2. Code won't compile

Expected behavior
Be able to implement the EncryptionService interface to be able to create my own service.

Additional context
N/A

Log lines inside Monos can miss contextual information

In a reactive environment the context view isn't always propagated to the first log line telling us if it's going to call the new flow or not.

Expecting a log line like this:
2023-04-14T14:01:46.024+02:00 INFO [traceid=643940a850dacb14e43c225e8f9b7dd2 spanid=d91873ce76003bb7] n.ShadowFlow : [instance=SomeService] Calling new flow: true

But the line misses the MDC:
2023-04-14T14:01:46.024+02:00 INFO [traceid=spanid=] n.ShadowFlow : [instance=SomeService] Calling new flow: true

Make README up-to-date

The README is currently quite outdated, as it even states that you have to build the library yourself.
Also, I think it would be best to stop focussing so much on the encryption, since probably a lot of potential consumers might not handle sensitive data.
The How To should probably be a bit higher up as well, to ensure people can see how it works in one glance.

Could you please make it up to date? :)

Make encryption for field differences optional

Is your feature request related to a problem? Please describe.
I understand that backends might contain sensitive information, so that you/we made the encryption mandatory, but in some cases it's not the case.

Describe the solution you'd like
Make the encryption optional in the builder, but have it activated by default.

Introduce a specific exception for shadow-flow

Is your feature request related to a problem? Please describe.
Let's assume a new flow server is unavailable, then a web-client throws java.net.ConnectException. And in usage, we can't distinguish where this exception happened and who throws it. But we would like to know in order to exclude or ignore for example, otherwise, it affects the whole response and behavior of a service. We expect seamless integration for shadow flow.

In order to reproduce shutdown your shadow-flow server.
There is an exception stack trace.

[instance=MyService] Failed to run the shadow flow
java.net.ConnectException: Connection refused
	at java.base/sun.nio.ch.Net.pollConnect(Native Method)
	at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:672)
	at java.base/sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:946)
	at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:337)
	at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:334)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:776)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:833)
Wrapped by: io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: localhost/127.0.0.1:8101
Wrapped by: org.springframework.web.reactive.function.client.WebClientRequestException: Connection refused: localhost/127.0.0.1:8101
	at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:136)
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ Request to POST null [DefaultWebClient]

Describe the solution you'd like
Introduce a specific exception for a new flow.

In a reactive context MDC values are not logged when comparing changes

The current code does subscribe a mono on a scheduler which will make it lose the context of the original mono.
Suggestion would be to defer the context and write it to the child mono.

    public Mono<T> compare(final Mono<T> currentFlow, final Mono<T> newFlow) {
        final var callNewFlow = shouldCallNewFlow();
        logger.info("{} Calling new flow: {}", instanceNameLogPrefix, callNewFlow);

        return currentFlow.doOnNext(currentResponse -> {
            if (callNewFlow) {
                newFlow.doOnNext(newResponse -> logDifferences(javers.compare(currentResponse, newResponse)))
                        .subscribeOn(scheduler)
                        .subscribe();
            }
        });
    }

In the above code (similarly for compareCollections) the line logger.info("{} Calling new flow: {}", instanceNameLogPrefix, callNewFlow); does contain MDC values, but the logger in #logDifferences(..) does not:

INFO [traceid=12345 spanid=54321] [ctor-http-nio-2] n.r.online.shadow_tool.ShadowFlow        : [instance=SomeService] Calling new flow: true
INFO [traceid= spanid=] [ctor-http-nio-5] n.r.online.shadow_tool.ShadowFlow        : [instance=CustomerHub] The following differences were found: ....

Suggested solution:

    public Mono<T> compare(final Mono<T> currentFlow, final Mono<T> newFlow) {
        final var callNewFlow = shouldCallNewFlow();
        logger.info("{} Calling new flow: {}", instanceNameLogPrefix, callNewFlow);

        return Mono.deferContextual(contextView ->
                currentFlow.doOnNext(currentResponse -> {
                    if (callNewFlow) {
                        newFlow.doOnNext(newResponse -> logDifferences(javers.compare(currentResponse, newResponse)))
                                .contextWrite(contextView)
                                .subscribeOn(scheduler)
                                .subscribe()
                        ;
                    }
                }));
    }

And similarly for compareCollections.

Include/exclude list of fields

Is your feature request related to a problem? Please describe.
When it's known that a new backend will have fewer or more fields it will always show differences. Perhaps adding an include and/or exclude list for such fields might help in reducing the list of differences.

Describe the solution you'd like
In the compare function/method or in the configuration I would expect a way to add a list of fields to exclude from comparing.

Describe alternatives you've considered
Ignore the diffs.

Additional context
None

Let the user provide their own Cipher

Is your feature request related to a problem? Please describe.
As an user of the shadow tool, i would like to provide my own cipher. Currently, i'm forced to use RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING

Describe the solution you'd like
Give the user an option to provide their own cipher. So they can choose which algorithm they would like to use.

A possible solution could be:

class EncryptionService {
    public static final String ALGORITHM = "RSA";
    public static final String ALGORITHM_MODE_PADDING = ALGORITHM + "/ECB/OAEPWITHSHA-256ANDMGF1PADDING";
    private final PublicKey publicKey;

    EncryptionService(final PublicKey publicKey) {
        this.publicKey = publicKey;
    }

    EncryptionService() {}

    String encrypt(final String value) {
        try {
            final var cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            return Base64.toBase64String(cipher.doFinal(value.getBytes()));
        } catch (final Exception e) {
            throw new SecurityException(e);
        }
    }

    String encrypt(final Cipher cipher, final String value) {
        try {
            return Base64.toBase64String(cipher.doFinal(value.getBytes()));
        } catch (final Exception e) {
            throw new SecurityException(e);
        }
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.