Ruby
Rails
AWS
S3
セキュリティ
OriginalAtraeDay 9

【CS Hack 第1弾!!】アプリケーション内でファイルの受け渡しを実装する

はじめに

皆さん、カスタマーサクセスしてますか?価値あるプロダクトを作っていますか?
日々自分たちのカスタマーがより良い活動をできるよう、プロダクトを開発・運用しているかと思いますが、活動の中でのヒューマンエラーを無くす動きも大切です。

カスタマーサクセスの為に、検証作業も含め、頻繁にカスタマーとデータのやり取りをする場合(例: 提案資料、データの受け渡し...)、うっかりミスが致命的な問題につながり兼ねません。
メールやFBで気軽に連絡できる時代において、ヒューマンエラーによる情報漏えいはかなり大きな割合を占めております。

leak07.jpg

引用:情報漏洩の原因を徹底解析!原因と結果から学ぶ意識改革

カスタマーのより良い活動を支援しようと思ったのに・・・、その活動がサービスの停止へと導いてしまった、、、なんて事があったら悲しすぎますね。
そんなミスを防ぐ為に、今日はアプリケーション内でファイルをDLする機構を作ることで、セキュアな環境下でのやり取りを実現してみたいと思います。

イメージ図

スクリーンショット 2017-12-09 12.05.27.png

実装

サーバ側

まずは、AWS側の設定を記述します。

config/initializers/aws_s3.rb
Aws.config.update({
    credentials: Aws::Credentials.new(
      #ACCESS_KEY,
      #SECRET_ACCESS_KEY
    ),
    region: #region情報を
    endpoint: #endpoint情報を
})

# もし参照パスにルールがあるなら記述しておきます
module AwsS3
  BUCKET = 'test'.freeze
  BASIC_URL = "https://s3.console.aws.amazon.com/s3/object/#{BUCKET}".freeze
end

ベースのモデルを作成。

class S3Client
  def initialize(*_)
    @client = Aws::S3::Client.new
  end
end

まずはS3にあるファイルを引っ張ってくる処理。
例えばログインしているユーザに応じて、S3のパスを分けます。

class S3Reader < S3Client
  def initialize(user)
    super
    @user_id  = user.id
  end

  def prefix
    "#{Rails.env}/#{@user_id}/"
  end

  def objects
    contents = @client.list_objects(bucket: AwsS3::BUCKET, prefix: prefix).contents
    contents.map { |content| S3Reader::Object.new(content, prefix) }
  end
end

# objectの加工用
class S3Reader::Object
  attr_reader :key, :name, :modified_at

  def initialize(content, prefix)
    @key = content.key
    @name = content.key.gsub(prefix, '')
    @modified_at = content.last_modified
  end
end

controller側に下記を実装。するとS3からDL可能なObjectが引っ張られてきて表示されます。

class DownloadFilesController < ViewBaseController
  def index
    s3 = S3Reader.new(@current_user)
    @objects = s3.objects.sort_by(&:modified_at)
  end
end

S3にアップロードしたファイルが・・・

スクリーンショット 2017-12-09 12.23.08.png

アプリケーション側でも閲覧可能に。

スクリーンショット 2017-12-09 12.23.50.png

引っ張るところまで出来れば次はクリック後、DLできるようにします。
DLの方法としては、一定時間だけオブジェクトにアクセスできるURLを作成し、send_dateをすることで、ブラウザ側にDLされるようにします。

参照)
一定時間だけS3のオブジェクトにアクセスできるURLを生成する

class S3Downloader < S3Client
  # 一時的なURLは120秒に設定
  def download_url(key)
    Aws::S3::Presigner.new(client: @client).presigned_url(
      :get_object, bucket: AwsS3::BUCKET, key: key, expires_in: 120
    )
  end

  def content(key)
    S3Downloader::Object.new(open(download_url(key)), key)
  end
end

class S3Downloader::Object
  attr_reader :content, :key

  def initialize(content, key)
    @content = content
    @key = key
  end

  def name
    @key.split('/').present? ? @key.split('/')[-1] : 'download'
  end

  def read
    @content.read
  end

  # ファイルの種類によってはsend_date時に上手くcontent_typeが出せない場合があるので、オーバーライドしておく
  def type
    @content.content_type
  end
end

後は、リストをクリックした際のアクションをController側に実装。

class DownloadFilesController < ViewBaseController
  def index
    s3 = S3Reader.new(@current_user)
    @objects = s3.objects.sort_by(&:modified_at)
  end

  def download
    downloader = S3Downloader.new
    content = downloader.content(params[:key])
    send_data content.read, filename: content.name, type: content.type
  end
end

これらを実施し、クリックすると・・・

ダウンロード.png

無事DLができました。

最後に

今回は第1弾ということで、まずはファイルをDLできる機構を作ってみました。
ログインすることでDLできるので、カスタマーには、『サービス内にアップロードしたので、DLしてくださいね。』と伝えるだけでいけますね。

ヒューマンエラーを出来る限り排除し、価値にフォーカスできる体制をつくるのもプロダクト開発において非常に重要だと感じています!

第二弾はこの機構に自動でファイルをアップロードしていく仕組みを構築します。
これで活用レポートだったり、定期的に送るデータは自動化してしまいましょう。