Удаление запрещенных 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, тип которого не соответствовал типу, объявленному в схеме валидации.