Cloudinaryの画像アクセス制御を使ってみる

#はじめに
今までのCloudinaryのサンプルでは、画像を登録したらすぐにインターネットに公開される状態で登録していました。自分が登録して自分で使う分には困ることはないですが、ユーザが画像を登録できるようなサービスに活用する場合は、公序良俗に反するような画像をアップロードされて、自分ドメイン経由で配信されるようなリスクもあるわけです。

ということで、今回はCloudinaryに用意されているアクセス制御機能を使ってみます。

##書いてあること

- delivery typeでのアクセスのコントロール
- sined urlの使い方
- access_modeとaccess contorolでのアクセス制御

サンプルコードはPHPで書いてあります。
Cloudinaryのフリーアカウント、API/SDKの環境が必要です。

#アクセス制御とは
まずは、アクセス制御の基本を抑えましょう。
今まで何気なくアップロードしてきた画像は、暗黙のうちにtype=upload, access_mode=publicとして登録されています。この辺を分解していきます。

## type パラメータ
typeパラメータはドキュメント上ではdelivery typeと記載されていて、URLの一部になる名称です。
URLを組み立てるときに、何という配信タイプのアセットを返すのか、という意味です。
主に使うdelivery typeは upload, authenticated, privateの3種類あります。

一番メジャーなuploadは、自分でアップロードした画像をpublic状態で置いておくものです。
>The default delivery type for uploaded assets. In most cases, this delivery type indicates that the asset is publicly available. However, there are options you can use to restrict access to assets with an upload delivery type. This includes strict transformations and access mode settings)

意訳すると、uploadは、アップロードしたアセットのデフォルトのデリバリタイプ。大半のケースにおいて、このデリバリータイプはpublicにアクセスできる。しかし、uploadと一緒に使えるアクセスを制限するオプションもあるよ。詳しくは、strict transformationとaccess modeの設定を見てね

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/06a98df5-b94f-3612-952f-e63b9cc82227.png)

---

authenticatedは、認可があれば見えるものです。
>Both original and transformed versions of the asset can be accessed only with a signed URL or an authentication token.

意訳すると、オリジナル画像と変換済み画像の両方とも、signed URLの仕組みか認可用トークンがあればアクセスできるよ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/847d9176-2c27-9caa-8f76-2494a82f32c7.png)

---

privateは、基本的に公開しないもの

>The URL for the original asset can be accessed only with a signed URL. Transformed versions of the asset are publicly available (unless strict transformations are also defined).
意訳すると、オリジナル画像はsigned URLでのみアクセス可能で、変換済みのバージョンは一般に公開する形になるよ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/2b89bcaa-e129-e6d7-a422-1e40cff2e7ef.png)

---

と使い分けます。upload以外を設定するのは**API(SDK)を使わないとできません**。登録後はMedia Libraryで見分けができます。

:sunflower: typeの一覧はこちらにあります。
https://cloudinary.com/documentation/image_transformations#delivery_types

##access_mode パラメータ
Delivery Type以外にもaccess_modeの値で、画像の公開（制限）状態を制御できます。access_modeは、public, authenticatedの2つがあります。

**public**
publicは、文字通り制限なくインターネットに公開されている状態のことで、何も指定しないとこの状態です。

**authenticated**
access_modeのauthenticatedは、Delivery Typeのauthenticatedと同じ動きです。Signed URLかトークンの仕組みで制限されるもので、正面突破はできません。

access_modeパラメータも基本的には、**API(SDK)を使わないと変更することができない**です。
マニュアルにもあるとおり、

>The authenticated delivery type is an element of the URL. Therefore, an asset uploaded with this type is always an authenticated asset. Changing the type also requires changing the delivery URL, which can be done using the to_type parameter of the Rename method.
The access_mode or access_control parameter values can be modified to set an asset either as requiring authentication or as publicly available, without modifying the URL (the delivery type remains upload).

authenticatedという単語はURLの一部になります。このタイプでアップロードしたものは閲覧に認可が必要です。APIのRenameメソッドを使えばauthenticatedからuploadにデリバリータイプを変更することができるけど、URLも変わるからね。
access_modeもしくはaccess_controlパラメータの値を使うと、URLの変更をすることなく画像の公開状態（公開か制限つきアクセスか）を変更することができますよ。デリバリータイプはuploadのままでね。

ということで、ユースケースとしては、将来的に制限なし（upload）で配信はするだろうけど、いったん制限付きにしておきたいときはtype=upload and access_mode=authenticatedにするのがよくて、将来にわたって制限配下におくならtype=authenticatedでよさそうです。

:page_facing_up:マニュアルはこちらです。
https://cloudinary.com/documentation/control_access_to_media#authenticated_access_to_media_assets

ただですねぇ、access_modeはdeprecatedなんですよ～ってGIT内では言われてるので、いつまで使えるのかは分かりません。
https://github.com/cloudinary/cloudinary_php/pull/169#issuecomment-592612270

#publicではない画像はどうなるか？
さて、publicでない画像を作ると何が起きるかですね。試してみるのが早いと思うので、アップロードした画像ファイル名でリンクを置いてみました。

実際にクリックしてもらうとわかりますが、最後の二つは見えません。404ですがちゃんと置いてあります。

:one: uploadは見える
https://res.cloudinary.com/kanaxx/image/upload/test-ac/public.png

:two: authenticatedは見えない
https://res.cloudinary.com/kanaxx/image/authenticated/test-ac/auth.png

:three: privateも見えない
https://res.cloudinary.com/kanaxx/image/private/test-ac/private.png


| type | access_mode | memo |
|:--|:--|:--|
| upload  | public  | :one:特に指定しないとこのパターン。URLを指定すると普通に見える  |
| upload  | authenticated  | 認可が必要  |
| authenticated  | public  | :two:認可が必要  |
| authenticated  | authenticated  |認可が必要  |
| private  | public  | :three: 認可が必要。変換した画像は見える  |
| private  | authenticated  | 認可が必要 |

と、こうなります。全て「認可が必要」っていう面白くない答えになります。


##画像を参照するための認可

Cloudinaryのアクセス認可には、この3種類があります。

- Signed URL方式
- TokenBased方式[^sign1]
- CookieBased方式[^sign2]

Signed URL以外の方法は有料プランでしか使えません。ということSined URL方式に絞ります。

[^sign1]: https://cloudinary.com/documentation/control_access_to_media#token_based_authentication
[^sign2]: https://cloudinary.com/documentation/control_access_to_media#cookie_based_authentication

##Signed URLとは
画像配信のURLに、**API Secretを使ったハッシュ値**を混ぜ込む方法です。Cloudinaryアカウントの関連付いたAPIシークレットを知らないと組み立てられないので、仮に画像ファイル名を知っていても、悪意を持ってURLを組み立てることができません。

authenticatedで登録した画像をMedia Libraryで確認するとわかりますが、URLに記号が混ざっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/faea48d0-061d-5126-adb6-9b1793dbb10b.png)

URL
[https\://res.cloudinary.com/kanaxx/image/authenticated/**s--AC7AZcoi--**/v1587651506/test-ac/auth.png](https://res.cloudinary.com/kanaxx/image/authenticated/s--AC7AZcoi--/v1587651506/test-ac/auth.png)

`AC7AZcoi`の部分がSignに当たります。
Signより後ろの部分を1文字でも変更すると、404エラー(Resource not found)になります。

:warning: 注意点
同じURLから組み立てるSigned URLは、いつも同じハッシュ値になります。Signに有効期限があるわけではないので、URLをShareされて公開されてしまえば、誰でも見えます。
こういう性質ですので、例えば、ログインした後にしか閲覧できない画像をSined URL配信していたとしても、Sign(ハッシュ値)を含んだURLを丸ごとコピーして誰かに渡すと、ログインしていなくても画像を見ることはできます。

URLと画像が完全に保護されるわけではなくて、URLを組み立てることを難しくする仕組みと思ったほうがいいです。

##Signed URLの使い道
Sined URLはどんなところで使うのか？

- authenticatedの画像を配信するとき
- privateの画像を確認するとき
- uploadのpublicな画像に対して、URLパラメータでの変換を制限するとき

1のパターンは、ユーザが好き勝手アップロードした画像を一度authenticatedで保存しておき、即公開はできないようにしておく。画像を確認するがSigned URLで画像を閲覧し、問題ない画像をpublicに変更するようなプロセスを踏むとき。
3のパターンは、on-the-fly transformation(URLを書き換えると好きなように画像が作れてしまう機能)に制約を付けるときです。透かしを消されたり、サイズ違いの画像を大量に生成されたりするのが具合悪い時です。

:notepad_spiral: 3に関連するブログがあります（英語）
https://cloudinary.com/blog/on_the_fly_image_manipulations_secured_with_signed_urls


#Signの組み立て方
さて、ここからやっとテクニカル。
Signed URLは作成ルールが公開されているので、誰でも組み立てることができます。
https://cloudinary.com/documentation/control_access_to_media#signed_delivery_urls

**作成の手順**

- URLのdelivery type以降のフレーズを切り取る
- APIシークレットｗ末尾に付ける
- sha-1のハッシュを作る
- 8文字切り取る
- 文字を置換


フレーズの最後にAPIシークレットを付けているので、APIシークレットを知らないと正解のハッシュにたどり着かないようになっています。APIキーとAPIシークレットはCloudinaryアカウント内で複数作れるので、どちらで作っても有効な画像URLにできます。

##Signするプログラム
PHPで書くとこんな感じになります。

```php
<?php
//https://github.com/cloudinary/cloudinary_php/blob/master/src/Cloudinary.php

$cloudName='kanaxx';
$resourceType = 'image';
$deliveryType = 'authenticated';
$apiSecret = 'l0FOU0e_xxxxxx';

//1.version付き, 2.versionなし, 3.変換パラメータ付き
$list = ['v1587651506/test-ac/auth.png', 'test-ac/auth.png', 'w_50,h_50/test-ac/auth.png'];

foreach($list as $url ){
    $seedOfSign = $url . $apiSecret;
    $signature = sha1($seedOfSign, true);
    $signature64 = base64_encode($signature);
    $ans = substr(str_replace('+','-',str_replace('/', '_', $signature64)), 0, 8);
    echo $ans. PHP_EOL;

    $signedUrl = "https://res.cloudinary.com/{$cloudName}/{$resourceType}/{$deliveryType}/s--{$ans}--/$url";
    echo $signedUrl . PHP_EOL . PHP_EOL;
}


```

sha1するときに第2パラメータをtrueにして、バイナリデータを取得しないといけないところ、
sha1したあとにbase64をするだけでなく、+を-に置換し、/を_に置換する手順が必要です。ドキュメントに書いてないからSDKのソースから拾いました。

##Signの実行結果
3種類のURLをSignしたURLです。閲覧可能です。

**バージョン付きのURL　(v1587651506/test-ac/auth.png)**
https://res.cloudinary.com/kanaxx/image/authenticated/s--8aI0ntUq--/v1587651506/test-ac/auth.png

**バージョン無しURL (test-ac/auth.png)**
https://res.cloudinary.com/kanaxx/image/authenticated/s--03pgTcMn--/test-ac/auth.png

**変換パラメータ入りURL　(w_50,h_50/test-ac/auth.png)**
https://res.cloudinary.com/kanaxx/image/authenticated/s--QNJbgCH7--/w_50,h_50/test-ac/auth.png

#Helperクラスを使う
まぁ、Singedやauthenticatedの動きを把握するために、面倒な手順をやってきましたけど、HTML内に\<img>タグのsrcとして画像のURLを貼り付るには、こんな面倒な作業は不要です。
各言語のSDKに用意されているHelperクラスを使えば、Sign済みのURLが自動生成されます。

##簡単なHelperのサンプル

```php
<?php
require_once "vendor/autoload.php";
\Cloudinary::config([ 
    "cloud_name" => "kanaxx", "secure" => true,
    "api_key" => "14757xxxxx", "api_secret" => "l0FOU0e_3xxxxx", 
]);

//public_id=test-ad/auth.png のバージョンありURL
$publicId = 'v1587651506/test-ac/auth';

//画像の変換はせず、signするだけ
echo cloudinary_url(
    $publicId,
    ['type'=>'authenticated','sign_url' => true, 'force_version'=>false, 'format'=>'png']
) . PHP_EOL;
//画像の変換はせず、signしたものを<img>タグで
echo cl_image_tag(
    $publicId,
    ['type'=>'authenticated','sign_url' => true, 'force_version'=>false, 'format'=>'png']
) . PHP_EOL;

```

###実行結果
実行すると、このような結果になります。
タグ付きで出力すると、css系のclassを指定するのが難しくなるので、URLだけを取り出すのがいいでしょう。

```console
λ php cltag.php
http://res.cloudinary.com/kanaxx/image/authenticated/s--8aI0ntUq--/v1587651506/test-ac/auth.png
<img src='http://res.cloudinary.com/kanaxx/image/authenticated/s--8aI0ntUq--/v1587651506/test-ac/auth.png' />
```

##変換パラメータをHelperで指定する
Cloudinaryを使うなら、変換済みの画像を置くのではなく、出力直前に変換するのがいいですよね。
ということで、変換パラメータ付きでsined urlを作るコードも置いておきます。`raw_translation`を指定すると、URLの中に含む文字をそのまま指定できます。

```php
<?php
require_once "vendor/autoload.php";
\Cloudinary::config([ 
    "cloud_name" => "kanaxx", "secure" => true,
    "api_key" => "147571xxxxx", "api_secret" => "l0FOU0e_xxxxx", 
]);

//public_id=test-ad/auth.png のバージョンありURLと無しURL
$publicId = 'test-ac/auth';

//変換するURLをPHPのパラメータで組み立ててsign
echo cloudinary_url(
    $publicId,
    [
        'type'=>'authenticated',
        'sign_url' => true, 
        'force_version'=>false,
        'format'=>'png',
        'width'=>50, 
        'height'=>50,
    ]
) 
. PHP_EOL
. PHP_EOL;

//変換パラメータを文字列化してsign
echo cloudinary_url(
    $publicId,
    [
        'type'=>'authenticated',
        'sign_url' => true,
        'force_version'=>false,
        'format'=>'png',
        'transformation'=>['raw_transformation'=>'w_50,h_50'],
    ]
) . PHP_EOL;

```

```console
php cl_image_tag2.php
http://res.cloudinary.com/kanaxx/image/authenticated/s--dRL2v5BP--/h_50,w_50/test-ac/auth.png
http://res.cloudinary.com/kanaxx/image/authenticated/s--QNJbgCH7--/w_50,h_50/test-ac/auth.png
```


SDKのソースがGITに公開されていますので、興味があれば。[^git1] [^git2]

[^git1]: helperのcloudinary_url()メソッドhttps://github.com/cloudinary/cloudinary_php/blob/e1bbd148750eb2fc7ccc8203e15184cbbe9c9a7e/src/Helpers.php#L570

[^git2]: Cloudinary::cloudinary_url()　https://github.com/cloudinary/cloudinary_php/blob/e1bbd148750eb2fc7ccc8203e15184cbbe9c9a7e/src/Cloudinary.php#L1106


マニュアルはこちらにあります。
https://cloudinary.com/documentation/image_transformations#embedding_images_in_web_pages


#access_controlパラメータ
ちょっと毛色が違うアクセス制御の仕組みがaccess_controlです。

`Applying the access_control parameter (Premium feature)` [^ac1]
って、マニュアルには書いてあったのですけど、サポートに問い合わせたところ、フリープランでも使えるみたいです。ただし、フリープランで使えるのはanonymousの時間制限だけで、tokenは使えません。

[^ac1]: https://cloudinary.com/documentation/dam_manage_individual_assets#access_control_modes

Media Libraryではこのように見えます。右下アイコンは鍵マークです。
![2020-04-25_13h28_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/19a7441c-415c-1905-704f-552b090bf2ff.png)

画像を見ると、Restrictの状態になっていて、ボタンを押すことができるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/609ebacf-5b88-5f88-2abb-4c8ae9785a84.png)

ボタンを押すと有効期限の変更や解除（public）ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a771df5c-7560-cc46-4ece-5c2a06143622.png)

Access Controlプロパティを見ても、クリックできない形で出てくることもあるのですが、ここはちょっと不明です。
![2020-04-25_13h25_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/5fb7ae70-52d7-772b-3a36-bdb98cd5a828.png)


##コード
Access Controlを設定するコードです。Upload APIでアップロード時に設定するか、Admin APIのResources/updateで変更します。

```php
<?php
require_once "vendor/autoload.php";
\Cloudinary::config([ "cloud_name" => "kanaxx", "secure" => true,
    "api_key" => "147571xxxxx", "api_secret" => "l0FOU0e_xxxxxx", 
]);

//upload時にaccess_controlを設定する
$test1 = \Cloudinary\Uploader::upload('c:/tmp/images/ac-public.png',
    ['folder'=>'test-ac', 'public_id'=>'ac-public', 'type'=>'upload',
    'access_control'=>[
        "access_type"=>"anonymous", 
        "start"=>"2020-01-01T00:00+09:00", 
        "end"=>"2020-05-30T00:00+09:00"
    ], 
    ]
);
var_export($test1);
echo PHP_EOL;

//publicでアップロードして
$test2 = \Cloudinary\Uploader::upload('c:/tmp/images/test2.png',
    ['folder'=>'test-ac', 'public_id'=>'ac-set-admin', 'type'=>'upload','format'=>'jpg',]
);
var_export($test2);
echo PHP_EOL;

//↑で登録した画像(test-ac/ac-set-admin)を、admin APIのupdateでrestrictに変更する
$api = new \Cloudinary\Api();
$response = $api->update(
    'test-ac/ac-set-admin', 
    ['type'=>'upload', 
     'access_control'=>[
        "access_type"=>"anonymous",
        "start"=>"2019-11-11T00:00+09:00", 
        "end"=>"2020-11-11T00:00+09:00",]
    ]
);
var_export($response);

```

###実行結果
2019/11/11 - 2020/11/11 までの制限がかかりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/8ed1e060-7a83-b7f2-2506-e5c98760d913.png)

この画像が見えるのも2020/11/11までです。
https://res.cloudinary.com/kanaxx/image/upload/v1587795082/test-ac/ac-set-admin.jpg



#まとめ
Cloudinaryのアクセス制御についてまとめました。画像を消さずに閲覧不可にするには、access_controlって閲覧可能期間を過去にするのがよさそうです。有料プランで使えるtokenやcookieでのアクセス制御は、ログインと連携して画像の閲覧を制御できそうなので、ちゃんと使いこなせばサービス側での実装が減らせるかもしれません。
マニュアルは充実しているのはとてもいいのですけど、散在していて調べるのも試すのもなかなか大変でした。