Skip to content

Commit 63212a2

Browse files
Merge pull request #18 from philiplambok-mekari/KRED-2061-v3
KRED-2061: add feature to masking log
2 parents 2bfb1ea + 74403cc commit 63212a2

6 files changed

Lines changed: 383 additions & 11 deletions

File tree

lib/midtrans_api/client.rb

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
require 'midtrans_api/api/channel/list'
1818

1919
require 'midtrans_api/middleware/handle_response_exception'
20+
require 'midtrans_api/middleware/faraday_log_formatter'
2021

2122
require 'midtrans_api/model/base'
2223
require 'midtrans_api/model/check/status'
@@ -55,17 +56,10 @@ def initialize(options = {})
5556

5657
logger = find_logger(options[:logger])
5758
if logger
58-
connection.response :logger, logger, { headers: @config.log_headers, bodies: true } do |log|
59-
filtered_logs = options[:filtered_logs]
60-
if filtered_logs.respond_to?(:each)
61-
filtered_logs.each do |filter|
62-
log.filter(/(#{filter}":\s*")(.*?)(")/i, '\1[FILTERED]\3')
63-
log.filter(/(#{filter}":\s*)(\d+(?:\.\d+)?|true|false)/i, '\1[FILTERED]')
64-
log.filter(/(#{filter}":\s*)(\[.*?\])/i, '\1[FILTERED]')
65-
log.filter(%r{(#{filter}=)([\w+-.?@:/]+)}, '\1[FILTERED]')
66-
end
67-
end
68-
end
59+
connection.response :logger, logger,
60+
full_hide_params: options[:filtered_logs] || [],
61+
mask_params: options[:mask_params] || [],
62+
formatter: MidtransApi::Middleware::FaradayLogFormatter
6963
end
7064
end
7165
end

lib/midtrans_api/json_masker.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
module MidtransApi
2+
class JsonMasker
3+
def self.mask(json, options = {})
4+
return json unless json.is_a?(String)
5+
return json if json.empty?
6+
7+
output = JSON.parse(json)
8+
MidtransApi::JsonMasker.new(output, options).to_masked
9+
rescue JSON::ParserError
10+
json
11+
end
12+
13+
def initialize(data, options = {})
14+
@data = data
15+
@options = options
16+
@mask_params = options[:mask_params] || []
17+
@full_hide_params = options[:full_hide_params] || []
18+
end
19+
20+
def to_masked
21+
return @data if @mask_params.empty? && @full_hide_params.empty?
22+
23+
case @data
24+
when Array
25+
@data.map do |item|
26+
if item.is_a?(Hash)
27+
MidtransApi::JsonMasker.new(item, @options).to_hash
28+
else
29+
item
30+
end
31+
end
32+
when Hash
33+
filter(@data)
34+
else
35+
@data
36+
end
37+
end
38+
39+
def to_hash
40+
filter(@data)
41+
end
42+
43+
private
44+
45+
# rubocop:disable Style/CaseLikeIf
46+
def filter(output)
47+
output.each do |key, value|
48+
output[key] = if value.is_a?(Hash)
49+
MidtransApi::JsonMasker.new(value, @options).to_hash
50+
elsif value.is_a?(Array)
51+
value.map do |item|
52+
if item.is_a?(Hash)
53+
MidtransApi::JsonMasker.new(item, @options).to_hash
54+
else
55+
item
56+
end
57+
end
58+
else
59+
mask_value(key, value)
60+
end
61+
end
62+
end
63+
# rubocop:enable Style/CaseLikeIf
64+
65+
def mask_value(key, value)
66+
full_hide_params_to_s = @full_hide_params.map(&:to_s)
67+
return '*****' if full_hide_params_to_s.include?(key.to_s)
68+
69+
mask_params_to_s = @mask_params.map(&:to_s)
70+
return value if mask_params_to_s.include?(key.to_s) == false
71+
72+
value = value.to_s
73+
return '*****' if value.length <= 5
74+
75+
unmasked = value[0..2]
76+
masked = value[3..-1].gsub(/./, '*')
77+
"#{unmasked}#{masked}"
78+
end
79+
end
80+
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'faraday/logging/formatter'
2+
require 'midtrans_api/json_masker'
3+
require 'midtrans_api/url_masker'
4+
5+
module MidtransApi
6+
module Middleware
7+
class FaradayLogFormatter < Faraday::Logging::Formatter
8+
MAX_LOG_SIZE = 10_000
9+
10+
def initialize(env = {})
11+
@logger = env[:logger]
12+
@options = env[:options]
13+
super(logger: env[:logger], options: env[:options])
14+
end
15+
16+
def request(env)
17+
masked_url = MidtransApi::UrlMasker.mask(env[:url].to_s, @options)
18+
@logger.info "#{env[:method].upcase} #{masked_url}"
19+
return if env[:request_body].to_s.empty?
20+
return if env[:request_body].to_s.size > MAX_LOG_SIZE
21+
22+
message = {
23+
body: MidtransApi::JsonMasker.mask(env[:request_body], @options)
24+
}
25+
@logger.info({ request: message }.to_json)
26+
end
27+
28+
def response(env)
29+
return if env[:response_body].to_s.empty?
30+
return if env[:request_body].to_s.size > MAX_LOG_SIZE
31+
32+
message = {
33+
status: env[:status],
34+
body: MidtransApi::JsonMasker.mask(env[:response_body], @options)
35+
}
36+
@logger.info({ response: message }.to_json)
37+
end
38+
39+
def exception(exc); end
40+
end
41+
end
42+
end

lib/midtrans_api/url_masker.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module MidtransApi
2+
class UrlMasker
3+
def self.mask(url, options = {})
4+
return url unless url.is_a?(String)
5+
return url if url.empty?
6+
7+
url = URI.parse(url)
8+
MidtransApi::UrlMasker.new(url, options).to_s
9+
rescue URI::Error
10+
url
11+
end
12+
13+
def initialize(url, options = {})
14+
@url = url
15+
@mask_params = options[:mask_params] || []
16+
@full_hide_params = options[:full_hide_params] || []
17+
end
18+
19+
def to_s
20+
filter(@url)
21+
end
22+
23+
private
24+
25+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
26+
def filter(url)
27+
query_params = URI.decode_www_form(url.query || '').to_h
28+
return url.to_s if query_params.empty?
29+
30+
query_params.each do |key, value|
31+
full_hide_params_to_s = @full_hide_params.map(&:to_s)
32+
mask_params_to_s = @mask_params.map(&:to_s)
33+
if full_hide_params_to_s.include?(key)
34+
query_params[key] = '*****'
35+
elsif mask_params_to_s.include?(key)
36+
value = value.to_s
37+
if value.length <= 5
38+
query_params[key] = '*****'
39+
next
40+
end
41+
42+
unmasked = value[0..2]
43+
masked = value[3..-1].gsub(/./, '*')
44+
query_params[key] = "#{unmasked}#{masked}"
45+
end
46+
end
47+
# Rebuild the URL with the masked query parameters
48+
masked_query = URI.encode_www_form(query_params)
49+
masked_url = url.dup
50+
masked_url.query = masked_query
51+
masked_url.to_s
52+
end
53+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
54+
end
55+
end

spec/lib/json_masker_spec.rb

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
require 'spec_helper'
2+
require 'midtrans_api/json_masker'
3+
4+
RSpec.describe MidtransApi::JsonMasker do
5+
describe '.mask' do
6+
it 'returns nil when input is nil' do
7+
expect(described_class.mask(nil, mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])).to be_nil
8+
end
9+
10+
it 'returns empty string when input is empty string' do
11+
expect(described_class.mask('', mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])).to eq('')
12+
end
13+
14+
it 'returns array when input is array' do
15+
expect(described_class.mask([], mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])).to eq([])
16+
end
17+
18+
it 'returns expected when data is an array' do
19+
parsed = [
20+
{
21+
card_number: '123456789012',
22+
cvv: '123',
23+
address: 'Jakarta',
24+
email: 'hello@email.com'
25+
},
26+
{
27+
card_number: '123456789012',
28+
cvv: '123',
29+
address: 'Jakarta',
30+
email: 'hello@email.com'
31+
}
32+
]
33+
34+
masked = [
35+
{
36+
'card_number' => '*****',
37+
'cvv' => '*****',
38+
'address' => 'Jakarta',
39+
'email' => 'hel************'
40+
},
41+
{
42+
'card_number' => '*****',
43+
'cvv' => '*****',
44+
'address' => 'Jakarta',
45+
'email' => 'hel************'
46+
}
47+
]
48+
49+
output = described_class.mask(parsed.to_json, mask_params: %w[email], full_hide_params: %w[card_number cvv])
50+
51+
expect(output).to eq(masked)
52+
end
53+
54+
it 'returns expected when data is an array and attribute symbol' do
55+
parsed = [
56+
{
57+
card_number: '123456789012',
58+
cvv: '123',
59+
address: 'Jakarta',
60+
email: 'hello@email.com'
61+
},
62+
{
63+
card_number: '123456789012',
64+
cvv: '123',
65+
address: 'Jakarta',
66+
email: 'hello@email.com'
67+
}
68+
]
69+
70+
masked = [
71+
{
72+
'card_number' => '*****',
73+
'cvv' => '*****',
74+
'address' => 'Jakarta',
75+
'email' => 'hel************'
76+
},
77+
{
78+
'card_number' => '*****',
79+
'cvv' => '*****',
80+
'address' => 'Jakarta',
81+
'email' => 'hel************'
82+
}
83+
]
84+
85+
output = described_class.mask(parsed.to_json, mask_params: %i[email], full_hide_params: %i[card_number cvv])
86+
87+
expect(output).to eq(masked)
88+
end
89+
90+
it 'returns expected with valid JSON' do
91+
parsed = {
92+
card_number: '1234567890123456',
93+
expiration_date: '12/23',
94+
cvv: '***',
95+
name: 'John Doe',
96+
address: 'Jakarta',
97+
external_id: '12398123123',
98+
information: {
99+
email: 'bill@john.com',
100+
account_number: '1092830182309123'
101+
},
102+
items: [
103+
{
104+
quantity: 89_821_823,
105+
amount: 15_000,
106+
email: 'john@bill.com',
107+
more_info: {
108+
email: 'hello@gmail.com',
109+
booking_id: '1234567890',
110+
page: 1,
111+
limit: 2
112+
}
113+
}
114+
]
115+
}
116+
117+
masked = {
118+
'card_number' => '123*************',
119+
'expiration_date' => '*****',
120+
'cvv' => '*****',
121+
'name' => 'Joh*****',
122+
'address' => 'Jakarta',
123+
'external_id' => '12398123123',
124+
'information' => {
125+
'email' => 'bil**********',
126+
'account_number' => '*****'
127+
},
128+
'items' => [
129+
{
130+
'quantity' => 89_821_823,
131+
'amount' => '*****',
132+
'email' => 'joh**********',
133+
'more_info' => {
134+
'email' => 'hel************',
135+
'booking_id' => '1234567890',
136+
'page' => 1,
137+
'limit' => 2
138+
}
139+
}
140+
]
141+
}
142+
143+
output = described_class.mask(parsed.to_json, mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])
144+
145+
expect(output).to eq(masked)
146+
end
147+
148+
it 'returns expected with invalid JSON' do
149+
data = 'this is invalid json'
150+
output = described_class.mask(data, mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])
151+
152+
expect(output).to eq(data)
153+
end
154+
end
155+
end

0 commit comments

Comments
 (0)