Удаление запрещенных JSON-элементов#

Рассмотрим ситуацию:

Существует схема сообщения, и ситуация, когда клиент (или backend-приложение) передает сообщение, содержащее в себе элементы, не объявленные в нашей схеме. Но нюанс заключается в том, что нам будет достаточно json-объектов, объявленных в нашей json-схеме, а остальные нам не так и важны. Блокировать все сообщение из-за этого кажется избыточным. Для подобных ситуаций можно использовать действие JsonTransformer, позволяющий удалять из сообщения элементы json'а, не соответствующие схеме валидации, и передавать дальше json без запрещенных элементов.

Рассмотрим на примере:

Существует схема schema.json, которая определяет все важные данные:

{
  "id": "jsonTransformer.json",
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "success": {
      "type": "boolean"
    }
  }
}

Необходимо удалять элементы сообщения, которые не соответствуют вышеописанной схеме как для запросов, так и для ответов.

Создадим профиль-заглушку stub.yml, которая будет выступать в качестве backend.

profile: json_transformer_stub
system:
  conn_count: 4096
  optional:
    log_level: debug
  wrk_count: 1
version: 2.0.3
service:
  service_main_proxy:
    - id: main
      listen:
        - port: 30000
      url: /test
  service_static:
    - id: 1
      name: forbidden_element_service
      url: /test1
      optional:
        # forbiddenElement - элемент, наличие которого вызовет ошибку валидации по схеме
        response_body: '{"success": true, "forbiddenElement": "forbiddenValue"}'
        response_code: 200
    - id: 2
      name: invalid_json
      url: /test2
      optional:
        # в данном ответе помимо запрещенного элемента не пройдет валидацию по схеме также значение ключа "success", т.к. не является типом boolean(в контексте json-схемы)
        response_body: '{"success": 1, "forbiddenElement": "forbiddenValue"}'
        response_code: 200

Здесь определяем два http-сервиса на 30000 порту. Сервис с именем forbidden_element_service возвращает json с корректным ключом "success" и одним запрещенным элементом "forbiddenElement". Сервис с именем invalid_json возвращает json, в котором значение ключа "success" не соответствует нашей json-схеме.

Создадим, сконфигурируем и запустим профиль-заглушку:

[sowacfg@9ac79da84966 jsonTransformer]# sowa-config --add-profile json_transformer_stub
Profile "json_transformer_stub" was successfully created
[sowacfg@9ac79da84966 jsonTransformer]# sowa-config -c stub.yml
Profile "json_transformer_stub" was successfully configured.
[sowacfg@9ac79da84966 jsonTransformer]# sowa-config -r json_transformer_stub
Profile "json_transformer_stub" was successfully started.

Далее создадим профиль json_transformer, который будет отвечать за удаление запрещенных элементов в телах запросов/ответов.

Конфигурационный файл выглядит следующим образом:

profile: json_transformer
version: 2.0.3
system:
  conn_count: 4096
  wrk_count: 1
  optional:
    log_level: debug
    # объявление переменной $result_body, в которой будет содержаться значение тела оригинального запроса(в случае, когда удаление элементов не требуется) и значение тела запроса без запрещенных элементов(в случае, если какие-либо элементы были удалены из тела запроса)
    maps:
      - src_var: $clj_trimmed_body
        dst_var: $result_body
        volatile: true
    # по дефолту берем значение из переменной $clj_trimmed_body, а если она пустая, то из переменной $request_body
        mapping:
          - src_val: ''
            dst_val: $request_body
        default: $clj_trimmed_body
service:
  service_http_proxy:
    - id: json_transformer
      name: json.transformer
      description: Демонстрация работы action JsonTransformer
      url: ^\/test
      allowed_queries:
        - method: get
        - method: post
      listen:
        - port: 29999
      chains:
        request_chains:
          - actions:
                # action для валидации сообщения
            - action: JsonValidation
              params:
                validation_schema: {type: file, path: schema.json}
                # указываем ignore, т.к. в случае ошибки валидации обработка сообщения перейдет к action'у JsonTransformer
                action: ignore
            # условия вызова JsonValidation - запрос пришел с методом POST
              conditions:
                - var: $request_method
                  operator: 'in'
                  val: POST
            # объявление JsonTransformer action'а, с переданным ему параметром $clj_validation_error_text
            - action: JsonTransformer
              params: {elements_to_remove: $clj_validation_error_text}
            # условия вызова JsonTransformer'а - не пустая переменная с ошибками валидации
              conditions:
                - {operator: '!=', val: '', var: $clj_validation_error_text}
            message: $clj_request_body
        response_chains:
          - actions:
            # SetVar action нужен для того, чтобы очистить переменную clj_trimmed_body
            # Нужно это только для случая, когда у нас объявлено удаление запрещенных элементов для тел запросов
            - action: SetVar
              params:
                variables:
                  - name: clj_trimmed_body
                    value: ""
            # Далее все аналогично примеру из request_chains, за исключением сообщения, над которым необходимо производить манипуляции($clj_request_body -> $clj_response_body)
            - action: JsonValidation
              params:
                validation_schema: {type: file, path: schema.json}
                action: ignore
            - action: JsonTransformer
              params: {elements_to_remove: $clj_validation_error_text}
              conditions:
                - {operator: '!=', val: '', var: $clj_validation_error_text}
            message: $clj_response_body
        # важная часть конфигурационного файла, которая позволяет postprocessor'у переопределять тело ответа
        response_chain_properties:
          use_post_processor: true
          resp_gen_modify: 'on'
      upstream_group_id: 1
      upstream_groups:
        - id: 1
          servers:
            - server: 127.0.0.1:30000
      optional:
        # указываем из какой переменной брать тело запроса для его последующей передачи на backend
        # ранее в эту переменную мы поместили тело запроса после удаления из него запрещенных элементов
        proxy_set_body: $result_body
        # часть конфигурационного файла, позволяющая нам убедиться в изменении тела, отправленного на backend
        event_hook:
          stage:
            - name: upstream_request
          log_in_file: on
        # объявление постпроцессора, с помощью которого будет производиться изменение тела ответа клиенту
      postprocessors:
        postprocessor_error:
          code:
            - 500 # Internal Server Error
          pattern_file: errors_schema_v_1.0.json
          rules_file: postprocessor_rules_json_transformer.yml
          content_type: application/json;charset=UTF-8

Создадим файл с правилами postprocessor_rules_json_transformer.yml, содержащий в себе правила обработки ответов клиенту:

rules:
# правило, описывающее поведение при возникновении ошибок валидации запроса, которые не удалось обработать action'у JsonTransformer
  - condition:
      - var: clj_validation_error_code
        operator: ">"
        val: 0
      - var: clj_processing_phase
        operator: "="
        val: "REQUEST"
    replacements:
      - pattern: errorCode_variable
        val: "EFSGW-41"
      - pattern: errorText_variable
        val: "Ошибка валидации запроса"
    status: 500
    priority: 49
# правило, описывающее поведение при возникновении ошибок валидации ответа, которые не удалось обработать action'у JsonTransformer
  - condition:
      - var: clj_validation_error_code
        operator: ">"
        val: 0
      - var: clj_processing_phase
        operator: "="
        val: "RESPONSE"
    replacements:
      - pattern: errorCode_variable
        val: "EFSGW-42"
      - pattern: errorText_variable
        val: "Ошибка валидации ответа"
    priority: 50
    status: 500
# правило, использующееся для замены тела ответа с запрещенными элементами, на тело ответа, очищенное от запрещенных элементов
# в блоке condition описываются условия вызова данного правила
  - condition:
# переменная с ошибками валидации не пустая
      - var: clj_validation_error_text
        operator: "!="
        val: ""
# переменная, содержащая в себе значение тела ответа после удаления из него запрещенных элементов, не пустая
      - var: clj_trimmed_body
        operator: "!="
        val: ""
# фаза обработки запроса = RESPONSE
      - var: clj_processing_phase
        operator: "="
        val: "RESPONSE"
    replacements:
# заменим "value" на значение переменной "clj_trimmed_body"
      - pattern: value
        val: $clj_trimmed_body
# определение паттерна ответа
    responsePattern: "{value}"
    priority: 55
# проброс "оригинального" статуса ответа
    status: $upstream_status
# правило, которое вызовется в случае возникновения необработанной ошибки на этапе обработки ответа
  - condition:
      - var: clj_processing_phase
        operator: "!="
        val: ""
      - var: clj_response_chain_result
        operator: "in"
        val: "ERROR,DENY"
    replacements:
      - pattern: errorCode_variable
        val: "EFSGW-100"
      - pattern: errorText_variable
        val: "Неизвестная run-time ошибка"
    priority: 2
    status: 500
# правило, которое вызовется в случае возникновения необработанной ошибки на этапе обработки запроса
  - condition:
      - var: clj_processing_phase
        operator: "!="
        val: ""
      - var: clj_request_chain_result
        operator: "in"
        val: "ERROR,DENY"
    replacements:
      - pattern: errorCode_variable
        val: "EFSGW-100"
      - pattern: errorText_variable
        val: "Неизвестная run-time ошибка"
    priority: 1
    status: 500
  - default: True
    replacements:
      - pattern: errorCode_variable
        val: "EFSGW-100"
      - pattern: errorText_variable
        val: "Неизвестная run-time ошибка"
    status: 500
# флаг, отвечающий за пропуск правила, помеченного как "default". Данный флаг нужен из-за особенностей реализации
skipOnDefault: True

Создадим, сконфигурируем и запустим профиль json_transformer:

[sowacfg@9ac79da84966 jsonTransformer]# sowa-config --add-profile json_transformer
Profile "json_transformer" was successfully created
[sowacfg@9ac79da84966 jsonTransformer]# cp schema.json /sowa/profile_storage/custom/json_transformer/
[sowacfg@9ac79da84966 jsonTransformer]# cp postprocessor_rules_json_transformer.yml /sowa/profile_storage/custom/json_transformer/
[sowacfg@9ac79da84966 jsonTransformer]# sowa-config -c jsonTransformer.yml
Profile "json_transformer" was successfully configured.
[sowacfg@9ac79da84966 jsonTransformer]# sowa-config -r json_transformer
Profile "json_transformer" was successfully started.

Отправим запрос без тела на сервис "forbidden_element_service", используя в качестве proxy сервис из профиля json_transformer:

[sowacfg@9ac79da84966 jsonTransformer]$ curl -v -X GET http://localhost:29999/test1
* About to connect() to localhost port 29999 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 29999 (#0)
> GET /test1 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:29999
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 18 Sep 2020 11:42:42 GMT
< Content-Type: application/json;charset=UTF-8
< Content-Length: 16
< Connection: keep-alive
< Server: nginx-clojure
< Accept-Ranges: bytes
<
* Connection #0 to host localhost left intact
{"success":true}

В ответ вернулось тело, соответствующее схеме валидации.

Если посмотреть на логи, то можно увидеть записи, подтверждающие удаление элементов, не проходящих валидацию по схеме:

2020/09/18 11:42:42 [debug] 94032#94032: *11 [JsonTransformer] Forbidden elements:
2020/09/18 11:42:42 [debug] 94032#94032: *11 [JsonTransformer]   parentPath = '', forbiddenElement = 'forbiddenElement'
2020/09/18 11:42:42 [debug] 94032#94032: *11 [JsonTransformer] Removing forbidden elements
2020/09/18 11:42:42 [debug] 94032#94032: *11 [JsonTransformer] trimmedMessage = {"success":true}

В таком случае попробуем отправить запрос с json'ом, также содержащим в себе элементы, не объявленные в json-схеме:

[sowacfg@9ac79da84966 jsonTransformer]$ curl -v -X POST http://localhost:29999/test1 -d '{"success": true, "forbiddenElementRequest": "forbiddenValue"}'
* About to connect() to localhost port 29999 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 29999 (#0)
> POST /test1 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:29999
> Accept: */*
> Content-Length: 62
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 62 out of 62 bytes
< HTTP/1.1 200 OK
< Date: Fri, 18 Sep 2020 11:49:46 GMT
< Content-Type: application/json;charset=UTF-8
< Content-Length: 16
< Connection: keep-alive
< Server: nginx-clojure
< Accept-Ranges: bytes
<
* Connection #0 to host localhost left intact
{"success":true}

Видим, что вернулся точно такой же ответ. Давайте убедимся, что из запроса было удалено все, что не соответствует нашей json-схеме. Для этого заглянем в evh-лог upstream_request:

[sowacfg@9ac79da84966 jsonTransformer]$ cat /sowalogs/json_transformer/services/service_http_proxy/json_transformer/upstream_request/192dacfc90f030bbc989f807230800b9.log
{
    "eventTime": "18.09.2020 11:49:46,229 +00:00",
    "userSessionID": "EMPTY",
    "procStatus": "SUCCESS",
    "contentBody": "{\"success\":true}",
    "inboundURL": "http://localhost:29999/test1",
    "outboundURL": "http://127.0.0.1:30000/test1",
    "outboundServer": "127.0.0.1:30000",
    "protocolHeaders": {"Host": "127.0.0.1:30000","X-Real-IP": "127.0.0.1","Connection": "close","Content-Length": "16","User-Agent": "curl/7.29.0","Accept": "*/*","Content-Type": "application/x-www-form-urlencoded"},
    "domain": "json_transformer",
    "cookies": {},
    "protocolMethod": "POST",
    "multiProtocolGatewayName": "json_transformer",
    "LGBGroup": "1",
    "clientIP": "127.0.0.1",
    "targetValidator": "",
    "initDNCertificate": "",
    "originalHTTPResponseCode": "",
    "xGlobalTransactionID": "192dacfc90f030bbc989f807230800b9",
    "userActionID": "json.transformer"
}

Действительно, в contentBody указано сообщение, валидное json-схеме.

Попробуем теперь отправить запрос на сервис invalid_json:

[sowacfg@9ac79da84966 jsonTransformer]$ curl -X GET http://localhost:29999/test2
{
    "status": {
        "code": 3,
        "errors": [
            {
                "id": "EFSGW-42",
                "element": "",
                "title": "datapower",
                "description": "Ошибка валидации ответа"
            }
        ],
        "warnings": [
            {
                "id": "",
                "element": "",
                "title": "",
                "description": ""
            }
        ]
    }
}

Посмотрим в логи сервиса, и увидим следующую запись:

Caused by: org.bank.sowa.actions.base.ActionException: [JsonTransformer] Exception while removing forbidden elements. Message contains unhandled validation exceptions. [#/success: expected type: Boolean, found: Integer]

Видим ошибку, связаную с тем, что сообщение содержало в себе элемент success, тип которого не соответствовал типу, объявленному в схеме валидации.