Generic 4-eyes-principle for AdminAPI

CoreWallet provides a generic 4-eyes-principle feature for all non-read-only AdminAPIs. This document describes how to enable this feature, how the feature works and what restrictions exist.

Introduction

Some products require a 4-eye-principle for every change via AdminAPI due to regulations or control measures.

The generic 4-eyes-principle provides a single solution for existing and future AdminAPIs for both the CoreWallet and Product AdminAPIs:

  • Existing AdminAPIs require no change in their implementation, keeping them simple
  • Future AdminAPIs can be added without risking to forget to add/configure 4-eyes-principle for it

On a high level, the feature works as follows for AdminAPIs that create/write/update/trigger things:

  1. A user with ADMIN role makes an AdminAPI call to create/write/update/trigger something
  2. The API request is not executed but stored for approval
  3. A different user with ADMIN role approves the request
  4. The API request is executed

A request can be revoked by the original user, and declined by another user. In both cases, the request is never executed.

How does it work

In more detail, the feature works as follows regarding intercepting AdminAPI calls:

  • When receiving an API call, the HTTP request is examined to determine if it is an applicable AdminAPI call that needs 4-eyes-principle
    • By default HEAD/GET methods are ignored
    • By default requestUrls that start with the AdminAPI prefix are considered
    • By default pre-configured file-upload urls are ignored
  • The API call continues when the request is not relevant for 4-eyes-principle
  • The API call is intercepted if it is relevant for 4-eyes-principle
    • The API call details are stored in the DB using the ApprovableAdminActionService
      • This requires the caller to have ADMIN role
    • The API response contains the JSON-representation of this action
    • The API response contains a HTTP-header with the ID of this action
      • The AdminUI uses this header to recognize if the API call resulted in an action, or if it was executed normally

In more detail, the feature works as follows regarding approving an action:

  • The AdminAPI to approve an action is excluded from the request-interceptor
  • The action is loaded and validated
    • Ensure the approver is a different user than the creator of the action
    • Ensure the action can be approved
  • The action is executed by sending the request-details from the action in a HTTP request using the API-credentials of the approver
    • An HTTP header is added to the request to indicate this request has been approved
    • An HTTP header is added to the request containing an action-specific secret to prevent spoofing such approved requests (the secret is not exposed in the action API response)
  • The API interceptor examines the HTTP request, and executes it
    • It uses the given HTTP headers to determine this request is an approved action
    • It loads the action that was approved, and makes sure the request matches the action (including the secret)
  • The action is updated with the HTTP response of the action execution
  • HTTP 4xx errors succeed the approval and permanently fail the action (it cannot be retried)
  • HTTP 5xx and network errors fail the approval API call (so it can be retried later)

How to enable and configure 4-eyes-principle

The generic 4-eyes-principle feature is enabled by setting the following configuration property in the product deployment that contains the API controllers (webapp-external):

trimplement.wallet.server.wallet.webapp.external.four-eye-principle.enabled=true

This also requires the following configuration in the product deployment that contains the service implementation (webapp-internal):

trimplement.wallet.server.wallet.impl.four-eye-principle.base-url=https://<<domain>>

The base-url must be set to the URL where the API controllers are exposed, so that the service can execute the HTTP request during action approval. It should be the URL before the typical '/v2/admin/...' path.

How to configure what API calls are intercepted/ignored

The following configurations can be set in the product deployment that contains the API controllers (webapp-external) if needed:

trimplement.wallet.server.wallet.webapp.external.four-eye-principle.requests.excludeMethods=
trimplement.wallet.server.wallet.webapp.external.four-eye-principle.requests.additionalIncludePathFilters=
trimplement.wallet.server.wallet.webapp.external.four-eye-principle.requests.additionalExcludePathFilters=
trimplement.wallet.server.wallet.webapp.external.four-eye-principle.requests.additionalExcludeFileUploadPathFilters=

They configure the following:

  • excludeMethods - a comma-separated list of HTTP verbs that are ignored for interception (defaults to 'GET,HEAD')
    • If you do not want an action to be created for PATCH requests, you could add `,PATCH` to this list
  • additionalIncludePathFilters - a comma-separated list of ant-style paths that are intercepted (defaults to '/v*/*/admin/**')
    • If you only want to intercept 'v2' of your API, you could set the following value: '/v2/your-product/admin/**'
  • additionalExcludePathFilters - a comma-separated list of ant-style paths that are ignored for interception (defaults to empty)
    • If you want to exclude some API paths from 4-eyes-principle, you can configure a value such as '/v2/your-product/admin/ignored-paths/**'
  • additionalExcludeFileUploadPathFilters - a comma-separated list of ant-style paths that are ignored for interception when the HTTP verb is POST (defaults to empty)
    • If you have a file-upload API in your product, you should exclude that specific path, e.g. with such a value '/v2/your-product/files/*/upload'

How to provide custom request-matching logic

If the configuration properties are not sufficient for customizing which API requests are intercepted and which are not, then you can implement the interface FourEyesHandlerInceptorRequestMatcherStrategy and/or subclass the FourEyesHandlerInceptorRequestMatcherStrategyStatic to provide your own implementation.

The main method to implement is 'boolean shouldIntercept(HttpServletRequest request)' which should return true if the API call should be intercepted for 4-eyes-principle, and false if not.

Your implementation should be the '@Primary' bean in the application context, so it's wired into the interceptor FourEyesHandlerInterceptor properly.

How to provide custom request-execution logic

If you want to provide a custom implementation of executing an approved action, you can implement the interface ApprovableAdminActionExecutor and/or subclass the ApprovableAdminActionExecutorImpl to provide your own implementation.

Your implementation should be the '@Primary' bean in the application context, so it's wired into the ApprovableAdminActionAdminServiceImpl properly.

In addition, you should configure your bean-name in the following property:

trimplement.wallet.server.wallet.impl.four-eye-principle.actionExecutorBeanName=

How to customize the RestTemplate used in the request-executor

If you want to customize the RestTemplate used to execute approved requests (e.g. to adjust the HTTP connection pool, request timeouts, etc) you can define your own RestTemplate bean and alias it to 'wallet.impl.ApprovableAdminActionExecutor.default.restTemplate' so that it will be wired into the ApprovableAdminActionExecutorImpl properly.

How to add custom logic in the lifecycle of actions

You can implement the ApprovableAdminActionListener interface and/or subclass the DefaultApprovableAdminActionListener to add your own logic in the lifecycle of actions. The listener has methods for every state-change.

You can use the DelegatingApprovableAdminActionListener to use multiple listeners.

The following property determines which listener bean is wired into the service:

trimplement.wallet.server.wallet.impl.four-eye-principle.actionListenerBeanName=

How to store additional info in actions

UIs that use the AdminAPI (such as the AdminUI) may want to store additional info in the actions, so that they can later render a proper view showing what the action will do.

For this, the UI can define and send its own HTTP headers (e.g. X-AdminUI-action) with each API request. When the API request is stored as an action, these HTTP headers are stored with the action.

When fetching actions, the HTTP headers are returned in the response as well, so the UI can evaluate them to have the info it needs for rendering a proper view for this action.

Intercepted API details

Intercepted AdminAPI response

The following shows an example AdminAPI request/response for an intercepted request:

HTTP request

POST http://localhost:8080/api/v2/admin/wallet/wallets/AAEAAAMnDPp5W4BKUwAs2wVY:activate
{
  "description": "Activate this wallet"
}

HTTP response code is 202 Accepted

HTTP response headers (note the 'x-approval-required' header)

{
 "connection": "keep-alive",
 "content-type": "application/json;charset=UTF-8",
 "date": "Fri, 11 Dec 2020 10:11:56 GMT",
 "keep-alive": "timeout=60",
 "transfer-encoding": "chunked",
 "x-approval-required": "AAEAAACAeQ5iTCqC6dSKmott",
 "x-request-id": "rBIAAwAAAXZRSSVHLSugK96cEKk"
}

HTTP response body (contains an ApprovableAdminAction JSON)

{
  "apiVersion": "2.0",
  "data": {
    "id": "AAEAAACAeQ5iTCqC6dSKmott",
    "status": "Created",
    "creationDateTime": "2020-12-11T10:11:56Z",
    "initiator": {
      "type": "User",
      "id": "AAEAAAMnDPp5WEYYmeKFcmcA"
    },
    "finalizeDateTime": null,
    "finalizer": null,
    "error": null,
    "request": {
      "method": "POST",
      "uri": "/api/v2/admin/wallet/wallets/AAEAAAMnDPp5W4BKUwAs2wVY:activate",
      "queryString": null,
      "requestId": "rBMABQAAAXZloC7ILSugK96cEKk",
      "body": "{\n  \"description\": \"Activate this wallet\"\n}",
      "headers": {
        "referer": [
          "http://localhost:8080/api/swagger-ui.html"
        ],
        "content-length": [
          "43"
        ],
        "accept-language": [
          "en,en-US;q=0.7,de;q=0.3"
        ],
        "origin": [
          "http://localhost:8080"
        ],
        "host": [
          "localhost:8080"
        ],
        "connection": [
          "keep-alive"
        ],
        "content-type": [
          "application/json"
        ],
        "dnt": [
          "1"
        ],
        "accept-encoding": [
          "gzip, deflate"
        ],
        "accept": [
          "application/json"
        ],
        "user-agent": [
          "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:83.0) Gecko/20100101 Firefox/83.0"
        ]
      }
    },
    "response": null
  }
}

Approved AdminAPI response (success)

When an action is approved but the execution succeeds with a HTTP 1xx/2xx/3xx code, the API response is as follows:

HTTP response status is 200 OK

HTTP response body (action status is Successful)

{
  "apiVersion": "2.0",
  "data": {
    "id": "AAEAAACAeQ5iS_XYABV70Rb3",
    "status": "Successful",
    "creationDateTime": "2020-12-11T10:41:48Z",
    "initiator": {
      "type": "User",
      "id": "AAEAAAMnDPp5WEYYmeKFcmcA"
    },
    "finalizeDateTime": "2020-12-11T10:41:57Z",
    "finalizer": {
      "type": "User",
      "id": "AAEAAAMnDPp1gIowjm9SC2Q2"
    },
    "error": null,
    "request": {
      "method": "POST",
      "uri": "/api/v2/admin/wallet/wallets/AAEAAAMnDPp5W4BKUwAs2wVY:lock",
      "queryString": null,
      "requestId": "rBMABQAAAXZloC7ILSugK96cEKk",
      "body": "{\n  \"description\": \"Lock this wallet\"\n}",
      "headers": {
        "referer": [
          "http://localhost:8080/api/swagger-ui.html"
        ],
        "content-length": [
          "39"
        ],
        "accept-language": [
          "en,en-US;q=0.7,de;q=0.3"
        ],
        "origin": [
          "http://localhost:8080"
        ],
        "host": [
          "localhost:8080"
        ],
        "connection": [
          "keep-alive"
        ],
        "content-type": [
          "application/json"
        ],
        "dnt": [
          "1"
        ],
        "accept-encoding": [
          "gzip, deflate"
        ],
        "accept": [
          "application/json"
        ],
        "user-agent": [
          "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:83.0) Gecko/20100101 Firefox/83.0"
        ]
      }
    },
    "response": {
      "statusCode": 200,
      "requestId": "rBIAAwAAAXZRZJ75LSugK96cEL8",
      "body": "{\"apiVersion\":\"2.0\",\"data\":{\"walletId\":\"AAEAAAMnDPp5W4BKUwAs2wVY\",\"displayId\":\"AAEAAAMnDPp5W4BKUwAs2wVY\",\"creationDateTime\":\"2017-08-29T11:09:01Z\",\"countryCode\":\"DE\",\"feeGroupId\":null,\"limitGroupId\":null,\"subsidiaryId\":1,\"walletStatus\":\"Locked\",\"walletType\":\"Full\",\"accounts\":[],\"owner\":{\"type\":\"User\",\"id\":\"AAEAAAMnDPp5W4BKUwAs2wVY\"},\"description\":null,\"externalReferenceId\":null,\"userId\":\"AAEAAAMnDPp5W4BKUwAs2wVY\"}}",
      "headers": {
        "Transfer-Encoding": [
          "chunked"
        ],
        "Keep-Alive": [
          "timeout=60"
        ],
        "X-Request-ID": [
          "rBIAAwAAAXZRZJ75LSugK96cEL8"
        ],
        "Connection": [
          "keep-alive"
        ],
        "Date": [
          "Fri, 11 Dec 2020 10:41:57 GMT"
        ],
        "Content-Type": [
          "application/json;charset=UTF-8"
        ]
      }
    }
  }
}

Approved AdminAPI response (failure)

When an action is approved but the execution fails with a HTTP 4xx error, the API response is as follows:

HTTP response status is 200 OK

HTTP response body (action status is Failed and the response contains the failure details)

{
  "apiVersion": "2.0",
  "data": {
    "id": "AAEAAACAeQ5iTCqC6dSKmott",
    "status": "Failed",
    "creationDateTime": "2020-12-11T10:11:56Z",
    "initiator": {
      "type": "User",
      "id": "AAEAAAMnDPp5WEYYmeKFcmcA"
    },
    "finalizeDateTime": "2020-12-11T10:39:01Z",
    "finalizer": {
      "type": "User",
      "id": "AAEAAAMnDPp1gIowjm9SC2Q2"
    },
    "error": "FieldInvalid(WalletStatus)] WalletStatus must be equal to Created but is Active",
    "request": {
      "method": "POST",
      "uri": "/api/v2/admin/wallet/wallets/AAEAAAMnDPp5W4BKUwAs2wVY:activate",
      "queryString": null,
      "requestId": "rBMABQAAAXZloC7ILSugK96cEKk",
      "body": "{\n  \"description\": \"Activate this wallet\"\n}",
      "headers": {
        "referer": [
          "http://localhost:8080/api/swagger-ui.html"
        ],
        "content-length": [
          "43"
        ],
        "accept-language": [
          "en,en-US;q=0.7,de;q=0.3"
        ],
        "origin": [
          "http://localhost:8080"
        ],
        "host": [
          "localhost:8080"
        ],
        "connection": [
          "keep-alive"
        ],
        "content-type": [
          "application/json"
        ],
        "dnt": [
          "1"
        ],
        "accept-encoding": [
          "gzip, deflate"
        ],
        "accept": [
          "application/json"
        ],
        "user-agent": [
          "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:83.0) Gecko/20100101 Firefox/83.0"
        ]
      }
    },
    "response": {
      "statusCode": 400,
      "requestId": "rBIAAwAAAXZRYfGDLSugK96cEKI",
      "body": "{\"apiVersion\":\"2.0\",\"error\":{\"code\":3000006,\"error\":\"FieldInvalid\",\"message\":\"[FieldInvalid(WalletStatus)] WalletStatus must be equal to Created but is Active\",\"field\":\"WalletStatus\"}}",
      "headers": {
        "Transfer-Encoding": [
          "chunked"
        ],
        "X-Request-ID": [
          "rBIAAwAAAXZRYfGDLSugK96cEKI"
        ],
        "Connection": [
          "close"
        ],
        "Date": [
          "Fri, 11 Dec 2020 10:39:01 GMT"
        ],
        "Content-Type": [
          "application/json;charset=UTF-8"
        ]
      }
    }
  }
}

Feature restrictions

The generic 4-eyes-principle feature has the following restrictions:

  • It does not work with request-specific API-tokens for the approver (typically provided in Api-Token / Api-Token-Id / Api-Request-Key HTTP headers)
    • The action will be executed with the API-credentials of the approver, so these may not be request-specific
  • It works only for AdminAPIs
  • It does not work for file-upload AdminAPIs
    • It is not recommended to store file-uploads in the DB for approval as it may be very inefficient
  • The initial caller must have the ADMIN role and does not need sufficient rights to execute the action
    • The action is intercepted and not executed, so there is no access-control or further permission checks that would normally happen when executing that action
  • The approver must have the ADMIN role and needs sufficient rights to execute the original action
    • The action is executed in the security-context of the approver
  • Any audit-log entries (WalletNotifications) for an action are created with the approver identity
  • Bootstrapping an environment requires 2 ADMIN users to be setup (e.g. via DB-script), otherwise the AdminAPI cannot be used (an admin cannot approve its own requests)
  • There is no idempotency check - multiple identical requests result in multiple actions
  • There is no input validation when the API call is intercepted - the URL may not exist, the entity may not exit, the request parameters/body may be invalid
    • Such validation happens only after the request is approved during its execution
  • Request/response details may contain sensitive information, which is stored in the DB in the action
    • By default authentication HTTP headers are excluded, but a response body may contain API-credentials

Potential future improvements

The following future improvements may be considered:

  • Allow APIs to support a dry-run execution mode to allow request/input validation when the action is created
    • This may require per-API implementation effort, and is thus not present in general
    • This may add input validation and access control checks when creating an action
  • Request/response detail anonymization
    • Currently all request/response details are stored, which may include sensitive data (credentials, personal data)
  • Add idempotency support when creating actions
  • Add support for file upload AdminAPIs
    • This may require temporarily storing the uploaded files in object storage rather than in the DB with the action
  • Action expiration
    • Currently an Action is status Created can be approved/declined/revoked indefinitely
  • Add support for request-specific API tokens during approval