googleapis/google-cloud-goライブラリを使って署名付きURLを発行する

# 概要

* 検証に必要なサービスアカウントを準備する
* Signed URLの発行
* 失効時にURLにアクセスしたときの挙動
* Signed URLの再発行
* gsutilでSigned URLを発行する

# Signed URLとは

> バケットとオブジェクトに対するクエリ文字列認証のためのメカニズムです。署名付き URLは、時間制限のある読み取りや書き込みのアクセス権を、Googleアカウントを持っているかどうかにかかわらず、URLを知っている全員に許可するための手段です。

[署名付き URL](https://cloud.google.com/storage/docs/access-control/signed-urls)より

GCSに置いたファイルをGoogleアカウントがないユーザーに提供したい。ただ、期間に制限を設けたい。

などに使いたくて調べました。ダウンロードURLだけでなく、アップロード用URLの生成も可能です。

Amazon S3ではこの辺（[他ユーザーとのオブジェクトの共有](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/ShareObjectPreSignedURL.html)）のお話です。

# 検証

## 検証に必要なサービスアカウントを準備する

GCSに限った話ではないですが、GCPのAPIを利用するには必要な権限を持ったサービスアカウントが必要です。

GCPの認証・認可全般のお話はこのドキュメントにまとまっています。

[https://cloud.google.com/docs/authentication/production](https://cloud.google.com/docs/authentication/production)

プログラムからサービスアカウント情報を取得するにあたってはどこで実行するか、どう認証情報を保存しているかによって変わります。

ローカルで実行するにあたってはローカル実行用のサービスアカウントを用意し、必要な権限を付与しました。

```console

# サービスアカウントの作成
# Service account name must be between 6 and 30 characters (inclusive), must begin with a lowercase letter, and consist of lowercase alphanumeric characters that can be separated by hyphens
$ gcloud iam service-accounts keys create cli-cred.json --iam-account test-cli@test-p-toshi0607.iam.gserviceaccount.com

# サービスアカウントに対する権限の付与
# roleはできる限り絞ってください
$ gcloud projects add-iam-policy-binding test-p-toshi0607 --member "serviceAccount:test-cli@test-p-toshi0607.iam.gserviceaccount.com" --role "roles/owner"

# 認証情報をファイルとして保存
$ gcloud iam service-accounts keys create cli-cred.json --iam-account test-cli@test-p-toshi0607.iam.gserviceaccount.com

# 環境変数にクレデンシャルの保存場所を設定
# 
$ mv cli-cred.json $HOME/.config/gcloud/cli-cred.json 
$ export GOOGLE_APPLICATION_CREDENTIALS=$HOME/.config/gcloud/cli-cred.json
```

認証情報を取得するための方法もライブラリでいくつか提供されています。

たとえば、`golang.org/x/oauth2`パッケージの`google.FindDefaultCredentirals`など。

https://github.com/golang/oauth2/blob/master/google/default.go#L76

つぎの優先優先順位で認証情報を探してくれます。

上記のように環境変数に設定されたものが最優先されます。

実行環境やクレデンシャルの保存方法によって使い分けてください。

```
// FindDefaultCredentials searches for "Application Default Credentials".
//
// It looks for credentials in the following places,
// preferring the first location found:
//
//   1. A JSON file whose path is specified by the
//      GOOGLE_APPLICATION_CREDENTIALS environment variable.
//   2. A JSON file in a location known to the gcloud command-line tool.
//      On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
//      On other systems, $HOME/.config/gcloud/application_default_credentials.json.
//   3. On Google App Engine standard first generation runtimes (<= Go 1.9) it uses
//      the appengine.AccessToken function.
//   4. On Google Compute Engine, Google App Engine standard second generation runtimes
//      (>= Go 1.11), and Google App Engine flexible environment, it fetches
//      credentials from the metadata server.
```
https://github.com/golang/oauth2/blob/master/google/default.go#L61


## Signed URLの発行

簡素なサンプルコードを載せます。

```go
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"cloud.google.com/go/storage"
	"golang.org/x/oauth2/google"
)

const (
	bucketName = "surl"
	fileName   = "test1/cat_haru.jpg"
)

func main() {
	ctx := context.Background()

	// 必要最低限のスコープを指定してください。
	// ScopeFullControl grants permissions to manage your
	// data and permissions in Google Cloud Storage.
	// ScopeFullControl = raw.DevstorageFullControlScope

	// ScopeReadOnly grants permissions to
	// view your data in Google Cloud Storage.
	// ScopeReadOnly = raw.DevstorageReadOnlyScope

	// ScopeReadWrite grants permissions to manage your
	// data in Google Cloud Storage.
	// ScopeReadWrite = raw.DevstorageReadWriteScope
	creds, err := google.FindDefaultCredentials(ctx, storage.ScopeReadOnly)
	if err != nil {
	// サンプル用です。適切にエラーハンドリングしてください。
		panic(err)
	}
	conf, err := google.JWTConfigFromJSON(creds.JSON, storage.ScopeReadOnly)
	if err != nil {
	// サンプル用です。適切にエラーハンドリングしてください。
		panic(err)
	}

	expires, _ := time.Parse(time.RFC3339, "2019-05-27T09:33:00-00:00")
	// オプションの意味はこのドキュメントを参照してください。
	// https://cloud.google.com/storage/docs/access-control/signed-urls
	opts := &storage.SignedURLOptions{
		GoogleAccessID: conf.Email,
		PrivateKey:     conf.PrivateKey,
		Method:         http.MethodGet,
		Expires:        expires,
	}
	url, err := storage.SignedURL(bucketName, fileName, opts)
	if err != nil {
    // サンプル用です。適切にエラーハンドリングしてください。
		panic(err)
	}
	fmt.Println(url)
}


```

※`surl`というバケットに`test1/cat_haru.jpg`というオブジェクト名で画像ファイルを保存してある前提です。

## 権限不足で作成したURLにアクセスしたときの挙動

URL付与対象のオブジェクトにアクセスする権限がなくてもそれっぽいURLは発行できますが、サービスアカウントにオブジェクトにアクセスする権限がない旨が表示されます。

<img width="934" alt="insufficient_role.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46547/60f17e85-ad46-ede3-ae3f-b9e9ef5bf7f3.png">

test-cli@kouzoh-p-toshi0607.iam.gserviceaccount.comにroles/ownerを割り当てたあとにそのままroles/ownerを剥奪した状態で試せます。

```console
$ gcloud projects remove-iam-policy-binding test-p-toshi0607 \
      --member='serviceAccount:test-cli@kouzoh-p-toshi0607.iam.gserviceaccount.com' --role='roles/owner'
```

最低限必要な権限は`roles/storage.admin`です。`roles/storage.objectViewer`や`roles/storage.objectAdmin`でも`AccessDenied`エラーになります。

## 失効時にURLにアクセスしたときの挙動

<img width="923" alt="expired.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46547/a365958b-87e9-e7f5-18e5-31d79afb6e43.png">


## Signed URLの再発行

バケットのオブジェクトを指定し、新しい期限を設定して再度`storage.SignedURL()`を実行します。

同一設定のSigned URLを再発行しても発行されるURLは変わりません。

同一のオブジェクトに対して有効な複数のSigned URLも存在可能です。

指すオブジェクトは同じだが有効期限が異なるなど。

## gsutilでSigned URLを発行する

コマンドでも発行できます。コマンドのヘルプで必要十分な情報が提供されているので実行結果をそのまま貼ります。

```console

$ gsutil help signurl

NAME
  signurl - Create a signed url


SYNOPSIS

  gsutil signurl [-c <content_type>] [-d <duration>] [-m <http_method>] \
      [-p <password>] [-r <region>] keystore-file url...



DESCRIPTION
  The signurl command will generate a signed URL that embeds authentication data
  so the URL can be used by someone who does not have a Google account. Please
  see the `Signed URLs documentation
  <https://cloud.google.com/storage/docs/access-control/signed-urls>`_ for
  background about signed URLs.

  Multiple gs:// urls may be provided and may contain wildcards. A signed url
  will be produced for each provided url, authorized
  for the specified HTTP method and valid for the given duration.

  Note: Unlike the gsutil ls command, the signurl command does not support
  operations on sub-directories. For example, if you run the command:

    gsutil signurl <private-key-file> gs://some-bucket/some-object/

  The signurl command uses the private key for a service account (the
  '<private-key-file>' argument) to generate the cryptographic
  signature for the generated URL. The private key file must be in PKCS12
  or JSON format. If the private key is encrypted the signed url command will
  prompt for the passphrase used to protect the private key file
  (default 'notasecret'). For more information regarding generating a private
  key for use with the signurl command please see the `Authentication
  documentation.
  <https://cloud.google.com/storage/docs/authentication#generating-a-private-key>`_

  gsutil will look up information about the object "some-object/" (with a
  trailing slash) inside bucket "some-bucket", as opposed to operating on
  objects nested under gs://some-bucket/some-object. Unless you actually
  have an object with that name, the operation will fail.

OPTIONS
  -m           Specifies the HTTP method to be authorized for use
               with the signed url, default is GET. You may also specify
               RESUMABLE to create a signed resumable upload start URL. When
               using a signed URL to start a resumable upload session, you will
               need to specify the 'x-goog-resumable:start' header in the
               request or else signature validation will fail.

  -d           Specifies the duration that the signed url should be valid
               for, default duration is 1 hour.

               Times may be specified with no suffix (default hours), or
               with s = seconds, m = minutes, h = hours, d = days.

               This option may be specified multiple times, in which case
               the duration the link remains valid is the sum of all the
               duration options.

               The max duration allowed is 7d.

  -c           Specifies the content type for which the signed url is
               valid for.

  -p           Specify the keystore password instead of prompting.

  -r <region>  Specifies the `region
               <https://cloud.google.com/storage/docs/locations>`_ in
               which the resources for which you are creating signed URLs are
               stored.

               Default value is 'auto' which will cause gsutil to fetch the
               region for the resource. When auto-detecting the region, the
               current gsutil user's credentials, not the credentials from the
               private-key-file, are used to fetch the bucket's metadata.

               This option must be specified and not 'auto' when generating a
               signed URL to create a bucket.

USAGE
  Create a signed url for downloading an object valid for 10 minutes:

    gsutil signurl -d 10m <private-key-file> gs://<bucket>/<object>

  Create a signed url, valid for one hour, for uploading a plain text
  file via HTTP PUT:

    gsutil signurl -m PUT -d 1h -c text/plain <private-key-file> \
        gs://<bucket>/<obj>

  To construct a signed URL that allows anyone in possession of
  the URL to PUT to the specified bucket for one day, creating
  an object of Content-Type image/jpg, run:

    gsutil signurl -m PUT -d 1d -c image/jpg <private-key-file> \
        gs://<bucket>/<obj>

  To construct a signed URL that allows anyone in possession of
  the URL to POST a resumable upload to the specified bucket for one day,
  creating an object of Content-Type image/jpg, run:

    gsutil signurl -m RESUMABLE -d 1d -c image/jpg <private-key-file> \
        gs://bucket/<obj>
```


# 参考

* [署名付き URL](https://cloud.google.com/storage/docs/access-control/signed-urls)
* [プログラムを使用した署名済みURLの作成](https://cloud.google.com/storage/docs/access-control/create-signed-urls-program)
* [gsutil を使用した署名済みURLの作成](https://cloud.google.com/storage/docs/access-control/create-signed-urls-gsutil)