Rails 5.2 から導入されたActiveStorageは rails_blob_path
を使うことで署名つきURLへのリダイレクトを返してくれます。
# in view
rails_blob_path(object.image)
# => "/rails/active_storage/blobs/<signed_id>/<filename>"
# 上記のURLにアクセスするとサービスに応じた認証URLへリダイレクトしてくれる。
# たとえばGCSなら `https://storage.googleapis.com/<bucket-name>/xxxyyyzzz`
# というようなURLにリダイレクトされる。
認証URLには有効期限がついている(デフォルトで5分)ので、/rails/active_storage/blobs/**
にアクセスが来たときに適当に認証処理を挟むとファイルに対して簡易的なアクセス制御が実現できます。これを実現するには上記のエンドポイントを処理する ActiveStorage::BlobsController
を上書きすれば良いです。参考のために ActiveStorage::BlobsControllerのデフォルトの実装を見てみましょう。
rails/blobs_controller.rb at bc9fb9cf8b5dbe8ecf399ffd5d48d84bdb96a9db · rails/rails
# frozen_string_literal: true
# Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
# security-through-obscurity factor of the signed blob references, you'll need to implement your own
# authenticated redirection controller.
class ActiveStorage::BlobsController < ActiveStorage::BaseController
include ActiveStorage::SetBlob
def show
expires_in ActiveStorage::Blob.service.url_expires_in
redirect_to @blob.service_url(disposition: params[:disposition])
end
end
実はここのコメントにも**「アクセス制御をしたい場合は自前でリダイレクトのためのコントローラーを実装しろ」**と書いてあります。
というわけでapp/controllers/active_storage/blobs_controller.rb
ファイルを作成し、アクセス制御のコードを挿入します。アクセス制御の内容はサービスによって様々でしょうから適宜書き換えてみてください。
class ActiveStorage::BlobsController < ActiveStorage::BaseController
include ActiveStorage::SetBlob
def show
expires_in ActiveStorage::Blob.service.url_expires_in
if access_allowed?(@blob)
redirect_to @blob.service_url(disposition: params[:disposition])
else
head :forbidden
end
end
private
def access_allowed?(blob)
# サービスに応じたアクセス処理を記述する。
...
end
end
どのレコードにアクセスが来たのか
上記のコントローラーで参照できるインスタンスは基本的に@blob
だけで、Blob
オブジェクトはサービス固有の情報を一切持っていないのでこのままではアクセス制御は行えません。しかしながら、@blob
に一対多の関係で紐づいている@blob.attachments
が持つrecord
という参照を利用することで、元のレコードを(だいたい)特定できます。Attachment
が複数なので結局どのレコードに対するアクセスなのかは判明しませんが、基本的にはそのうちのどれか一つに対するアクセスが許可されるならば、ここでは認証URLを返して良いと思います(つまり blob.attachments.any? { ... }
が true
ならOK)。
単一のファイル(Blob
)を複数のレコードに対して紐づけることも想定されているためこのような実装になっているそうです( @scivola さんありがとうございます!)。そのような場合は単一のBlob
に対し複数のレコードと、「関係」を抽象化したAttachment
が存在するわけですが、複数のレコードのうちどれかに閲覧権を持つユーザーならファイル自体の閲覧権も持つでしょう。
ActiveStorage::BlobsController
ではどのレコードに対するアクセスなのか特定できないためこのような回りくどいことをしているわけですが、もしかしたらレコードを特定する方法があるかもしれません(し、ないなら導入しても良いんじゃないでしょうか?)。
expires_in
だけ変えたい
認証URLの有効期限(expires_in
)はコントローラーで直接変えてもいいですが、config/application.rb
等で ActiveStorage::Service.url_expires_in
を設定することでも変更できます。
require_relative 'boot'
require 'rails/all'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module HogeApp
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
ActiveStorage::Service.url_expires_in = 1.minute
end
end