Rails アプリで S3 の権限制御を外さずに署名付きURLの署名を省いてファイルアクセスする方法を調べた

## はじめに

Rails アプリを用いて、S3 の権限制御を外さずに署名付きURLを省いてアクセスする方法について調べたので、その方法をまとめておきます。

## 背景

S3 バケット内のファイルに権限制御を入れ、そのファイルに対してアクセスするとき、署名付きURLを用いることがあると思います。こんなURLですね。(クレデンシャルはマスクしています)

`https://example.s3-ap-northeast-1.amazonaws.com/foo/bar/piyo.txt/?X-Amz-Expires=-0000000000&X-Amz-Date=0000000000000000&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AAAAAAAAAAAAAAAAAAAA/00000000/ap-northeast-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=0000000000000000000000000000000000000000000000000000000000000000`

しかしURLについた署名が、外部ライブラリを使っているときにノイズになることがあります。

例えば本のビューアーや写真のアルバムのようなライブラリを Rails アプリ上で扱っていると、ライブラリでは画像のファイル群をファイル指定で逐一アクセスするというような処理が走ることがあります。

おおよそそのような外部ライブラリは、アプリの同サーバ上のパブリックにアクセスできるディレクトリに画像ファイル群がおかれ、そこに対してアクセスするというケースを想定して作られていることが多いと思います。外部ライブラリ側では画像ファイルについた署名を考慮せずアクセスし、想定通り動かない、ということが起きがちです。

そのため、権限制御を入れたままで、署名付きURLを省いた URL で S3 バケット内のファイルにアクセスできるようにできないかを調べていました。

## どうやったか

Rails アプリのルーティングで、S3 バケットのファイルパスをある種プロキシし、実際の S3 バケットの署名付きURLにリダイレクトするみたいな仕組みにしてみました。

以下、やりかたを簡単にまとめます。


### ユースケース

以下のファイルに、署名付きURLなしでリクエストしたいとします。

* バケットURL： `https://example.s3-ap-northeast-1.amazonaws.com`
* ファイルパス：`foo/bar/piyo.txt`

イメージとしては、Rails の URL を `https://rails-sample.com` としたら、以下URLで S3 ファイルにアクセスできるようにしたいです。

`https://rails-sample.com/remote_storages/proxy/foo/bar/piyo.txt`

### ワイルドカードセグメントを使った Rails のルーティングを作る

まず、ルーティング用のコントローラーを作りますが、その時のルーティング設定では、[ワイルドカードセグメント](https://railsguides.jp/routing.html#%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%B0%E3%83%AD%E3%83%96%E3%81%A8%E3%83%AF%E3%82%A4%E3%83%AB%E3%83%89%E3%82%AB%E3%83%BC%E3%83%89%E3%82%BB%E3%82%B0%E3%83%A1%E3%83%B3%E3%83%88)を使います。

ワイルドカードセグメントとは、最初にアスタリスク(`*`) がついた部分のパラメータのことで、ルーティングのある位置から下のすべての部分にパラメータを展開させるために利用できます。

例えば `RemoteStoragesController` というコントローラーで`#proxy`という get メソッドを作るとしたら、以下のようになります。

```ruby
resources :remote_storages, only: [] do
  collection do
    get 'proxy/*path', to: 'remote_storages#proxy', as: 'proxy'
  end
end
```

上記により、`path` には `foo/bar/piyo.txt` というようなスラッシュ有りのパラメータを渡すことができるようになります。

### コントローラー作成

次にコントローラーを作ります。以下では S3 へのアクセスは [fog](https://github.com/fog/fog) を使っていますが、AWS SDK を使っても良いと思います。

以下では `proxy` メソッドに渡ってきた `path` と拡張子 `format` に応じて、署名付きURLにリダイレクトするという処理を作っています。
検証してみてわかったのですが、ワイルドカードセグメントには `.txt` のような拡張子は渡ってきませんでした。代わりに、`format` というパラメータに `txt` という文字列が入ってくるため、メソッド内で再度ファイルパスを作り直しています。

また、アクセスキーやバケットは `Rails.application.secrets` で秘匿化すると良さそうです。

```ruby
class RemoteStoragesController < ApplicationController
  def proxy
    s3_bucket = Fog::Storage.new(
      provider: 'AWS',
      aws_access_key_id: 'xxx',
      aws_secret_access_key: 'xxx',
      region: 'ap-northeast-1'
    ).directories.get('example')
    
    # 拡張子は *path に入らず :format に入るためここで調整している
    path = "#{params[:path]}.#{params[:format]}"
    redirect_to s3_bucket.files.get_https_url(path, 1.minutes.since.to_i)
  end
end
```

これによって、以下にアクセスすると、

`https://rails-sample.com/remote_storages/proxy/foo/bar/piyo.txt`

以下ファイルパスの署名付きURLにリダイレクトするということができます。

`https://example.s3-ap-northeast-1.amazonaws.com/foo/bar/piyo.txt`

## おわりに

今回 Rails ルーティングにワイルドカードセグメントというものがあることを初めて知りました。

ただ、ワイルドカードで渡すとどんな文字列も渡せるようになるため、セキュリティを考慮してある程度のパラメータチェックは入れたほうが良いのかなと思います。

## 参考

* [Rails のルーティング - Railsガイド](https://railsguides.jp/routing.html#%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%B0%E3%83%AD%E3%83%96%E3%81%A8%E3%83%AF%E3%82%A4%E3%83%AB%E3%83%89%E3%82%AB%E3%83%BC%E3%83%89%E3%82%BB%E3%82%B0%E3%83%A1%E3%83%B3%E3%83%88)