VCR で外部 API へのリクエストをダンプするときに機密情報をマスクしたい

## はじめに

外部API への通信内容を yaml で記録し、モックを作りやすくする [vcr](https://github.com/vcr/vcr) が便利でよく使っています。

その中で、機密情報を伴うリクエストを vcr で取り扱うとき、機密情報をマスクする方法について調べました。

## 機密情報をマスクしたい

POST でユーザー名・パスワードをパラメータで送信することを例とします。
いずれもダミーの例として、以下とします。

- username: yamadataro
- password: test12345678

vcr を使うと、リクエストの内容が以下のような yaml ファイルで保存されるのですが、その際リクエストパラメータに `username=yamadataro&password=test12345678`  というように、機密情報が平文で残ってしまう課題がありました。

```yaml
- request:
    method: post
    uri: https://test.api.com/auth
    body:
      encoding: US-ASCII
      string: grant_type=password&username=yamadataro&password=test12345678
    headers:
      User-Agent:
      - Faraday v0.12.2
      Content-Type:
      - application/x-www-form-urlencoded
      Accept-Encoding:
      - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
      Accept:
      - "*/*"
```

外部に漏れても問題ない情報であればいいのですが、そうでない場合もあると思うので、なるべくマスクできればいいなと考えていました。

## filter_sensitive_data を使えばできる

調べたところ、[filter_sensitive_data](https://relishapp.com/vcr/vcr/v/1-10-1/docs/configuration/filter-sensitive-data) というメソッドがあるようでした。

vcr の config に設定を記載することで、マスクしたい文字列にマスクを掛けることができます。

以下の例ですと、

```ruby
VCR.config do |c|
  c.stub_with :fakeweb
  c.cassette_library_dir = 'cassettes'
  c.filter_sensitive_data('<USERNAME>') { 'yamadataro' }
  c.filter_sensitive_data('<PASSWORD>') { 'test12345678' }
end
```
先ほどの yaml は `yamadataro` と `test12345678` の部分がそれぞれマスクされます。`yamadataro` と `test12345678` は、`ENV['USERNAME']` や、`Rails.application.secrets.username` などの定数を割り当てて使うことが多いかなと思います。

```yaml
- request:
    method: post
    uri: https://test.api.com/auth
    body:
      encoding: US-ASCII
      string: grant_type=password&username=<USERNAME>&password=<PASSWORD>
    headers:
      User-Agent:
      - Faraday v0.12.2
      Content-Type:
      - application/x-www-form-urlencoded
      Accept-Encoding:
      - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
      Accept:
      - "*/*"
```

## filter_sensitive_data は文字列完全一致が必要そう

しかし、`filter_sensitive_data` では文字列完全一致をさせる必要があるというところで少しハマりました。

例えば、URL の中にリクエストパラメータがある場合、パラメータ内の文字列がエスケープされている場合があります。`example@example.com` をマスクしたい場合を考えます。

```ruby
VCR.config do |c|
  c.stub_with :fakeweb
  c.cassette_library_dir = 'cassettes'
  c.filter_sensitive_data('<USERNAME>') { 'example@example.com' }
end
```

しかし、URL 内のユーザー名にあたる文字列は、`example%40example.com` とエスケープされているため、`filter_sensitive_data` とマッチせず使えませんでした。

```yaml
# 中略
- request:
    method: post
    uri: https://test.api.com/auth
    body:
      encoding: US-ASCII
      string: grant_type=password&username=example%40example.com
# 中略
```

このように、 URL 内でエスケープされている文字列は、エスケープしてあげる必要がありました。例としては以下のようになります。

```ruby
VCR.config do |c|
  c.stub_with :fakeweb
  c.cassette_library_dir = 'cassettes'
  c.filter_sensitive_data('<USERNAME>') { CGI.escape('example@example.com') }
end
```

## ソースコード確認


上記の挙動について、念の為該当のソースコードも確認しました。

### filter_sensitive_data について

https://github.com/vcr/vcr/blob/221abe52989993865bde4c52a92ee12bb1eb6141/lib/vcr/configuration.rb#L225-L238

* `filter_sensitive_data` は `define_cassette_placeholder` に alias で登録されている
* `define_cassette_placeholder` が実行され、`interaction.filter!(orig_text, placeholder)` でオリジナルテキストと置き換え後テキストのフィルタリングが行われる

### filter! について

https://github.com/vcr/vcr/blob/221abe52989993865bde4c52a92ee12bb1eb6141/lib/vcr/structs.rb#L557-L581

* `text`, `replacement_text` がどちらも `to_s` されている
* `filter_object!` 呼ばれる
* `object.gsub!(text, replacement_text) if object.include?(text)` で、置き換え前のテキストが含まれていれば置換処理する


上記から、実際の置換処理は `gsub!` で実行されていますが、置換前・置換後の対象はどちらも文字列になってるので、完全一致させる必要がありそうでした。

## おわりに

使い方をドキュメントやソースコードから追って確認するのは勉強になりますね。継続して使い方を確認しようと思います。

## 参考

* [Filter sensitive data - Configuration - Vcr - VCR - Relish](https://relishapp.com/vcr/vcr/v/1-10-1/docs/configuration/filter-sensitive-data)