Cognito User Pool - ロールベースアクセス制御を CFn で構築する

~~[Amazon Web Services Advent Calendar 2017](https://qiita.com/advent-calendar/2017/aws) の**2日目**です。~~大変遅くなってしまい、申し訳ありません…

# 前書き

[re:Invent](https://reinvent.awsevents.com) は今年も盛り上がりましたね。キーノート中ずっと喋りっぱなしの Andy Jassy の喉が心配になるほど大量の新サービスが発表されて、個人的には [EKS](https://aws.amazon.com/jp/eks/), [Fargate](https://aws.amazon.com/jp/fargate/), [Comprehend](https://aws.amazon.com/jp/comprehend/), Lambda の Go 対応あたりが興味深いと思いましたが、本記事では既存サービスについて書きます。

ちょうど1年前、2016年12月に Cognito にロールベースのアクセス制御機能が追加されました。

- [新機能 – Amazon Cognito グループ、およびきめ細かなロールベースのアクセス制御 | Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/new-amazon-cognito-groups-and-fine-grained-role-based-access-control-2/)
- [ロールベースアクセスコントロール - Amazon Cognito](http://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/role-based-access-control.html)

チュートリアルとしては上の記事の通りなのですが、 API の仕様理解と構築自動化のため、これをすべて CloudFormation で構築した上で簡単なデモを動かしてみます。

# デモの仕様

こんな感じのものを作ってみました。

- Cognito User Pool に登録されたユーザーの username/password でログインできる
- ユーザー登録はここでは実装しない
- ログインすると Cognito Identity が発行した temporaly credentials を使って S3 バケットに置いてあるオブジェクトにアクセスできる
    - ここでは固定された1つのオブジェクト `CONTENT.md` のみとする
- ユーザーのカスタム属性 `role` の値に従って異なる IAM Role が assume され、 S3 バケットにどのようなアクセスができるかが変化する

| logged in? | user attribute: `custom:role`  | S3 (read) | S3 (write) |
|------------|--------------------------------|-----------|------------|
| No         | -                              | No        | No         |
| Yes        | viewer                         | Yes       | No         |
| Yes        | editor                         | Yes       | Yes        |

# 早速触ってみる

Angular で実装してみました！

[デプロイ済みのデモはこちら](http://cognito-rbac-demo-dev-bucket-1we3kshddjkfb.s3-website-us-west-2.amazonaws.com/public/)です。その[コードはこちら](https://github.com/y13i/cognito-rbac-demo)です。

`hello-viewer`, `hello-editor` というユーザーでログインできます。どちらもパスワードは `Password1234` です。

ログインすると、 `View` のページから S3 に置かれたオブジェクトの内容が見られます。せっかくなので Markdown をパースして表示しています。

`hello-editor` でログインした場合はこの内容を編集して更新できます。

また、 `Identity` のページでは、現在払い出されているロールを確認できます。ログイン前とログイン後、そしてログインしているユーザーによって異なるロールが払い出されていることを確認できると思います。

## CFn による構築

ほとんど [CloudFormation で Cognito](https://qiita.com/y13i/items/1923b47079bdf7c44eec) で書いたままです。

後述する理由によりカスタムリソースが必要なので、 Serverless Framework で一式をデプロイすることにしました。 [`serverless.yml` にゴリゴリとリソースを並べていきます](https://github.com/y13i/cognito-rbac-demo/blob/master/serverless.yml)。 ~~`serverless.yml` が500行超えるやつは大体友達。~~

各リソースについて、この要件に必要な部分をざらっとおさらいします。

### `UserPool`

ロールベースアクセス制御に使うカスタム属性を `Schema` で定義しています。今回は `role` というカスタム属性を追加しました。

なお、カスタム属性は必須ではなく、例えば `email` などデフォルトの属性を使ってロール割り当てを切り替えることも可能です。

### `UserPoolClient`

今回はクライアントが Web なので、 `GenerateSecret` は `false` です。

### `IdentityPool`

特になし。 `IdentityPoolName` にハイフンを含めないのが地味に不便です。

### `CognitoUserBasePolicy`, `CognitoUserAuthenticatedPolicy`

コンソールからポチポチして作ると付けられるポリシーを元に作ってます。

今回はポリシーについては authed/unauthed という分け方ではなく、差分を追加していくという方法で分けてみました。

### `UnauthenticatedRole`, `AuthenticatedRole`

これもコンソールポチポチロールを元に作ってます。 `AssumeRolePolicyDocument` が複雑なのでがんばりましょう。

`AuthenticatedRole` には今回の要件で必要な S3 へのアクセス権限などは一切追加しません（**重要**）。

### `ViewablePolicy`, `EditablePolicy`

S3 へのアクセス権をこちらで定義しました。

### `ViewerRole`, `EditorRole`

上の `ViewablePolicy`, `EditablePolicy` を付けたロールです。これらが今回のデモでログインした時に払い出されます。

### `RoleAttachment`

`UnauthenticatedRole`, `AuthenticatedRole` をここで `IdentityPool` に紐付けます。

本来であれば、このリソースの `RoleMappings` で `ViewerRole`, `EditorRole`も紐付けたいのですが、そうしていません。

なぜなら **CloudFormation は KeyValue の Key を動的に定義できない** という問題があるため、 `RoleMappings` のキー（`cognito-idp.<region>.amazonaws.com/<userPool>:<userPoolClient>` という形式）をどうやってもハードコードする必要が生じてしまうからです。

この問題はフォーラムでも話題に上がっています。

- [AWS::Cognito::IdentityPoolRoleAttachment - dynamic RoleMappings?](https://forums.aws.amazon.com/thread.jspa?messageID=793299)

### `IdentityPoolRoleMapping`

というわけで、上の問題を無理矢理に解決するためのカスタムリソースです。

`cognito-identity:SetIdentityPoolRoles` を行うには `iam:PassRole` の権限も必要です。地味にハマリポイント。

属性のマッチ条件は完全一致、部分一致、不一致、前方一致が選べるようです。後方一致があれば `email` のドメイン名で…ができて便利そうなのですが。

- [Amazon Cognito IdentityPoolRoleAttachment MappingRule - AWS CloudFormation](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-identitypoolroleattachment-mappingrule.html#cfn-cognito-identitypoolroleattachment-rulesconfigurationtype-mappingrule-matchtype)

### `Bucket`

ユーザーが編集できるオブジェクトの他に、今回はデモそれ自体の動作に必要なファイルもここに置きます。

ブラウザー上で動作する SDK から直接叩くので、 CORS 設定が必要です。

### `BucketPolicy`

`public/*` のみ全開放とします。

### `AwsConfiguration`

どうせカスタムリソースを使うならということで、 Cognito の認証に必要な情報も JSON にしてバケットに置いておきます。

こうすることでクライアントのコードに Cognito 関係のリソースの ID をハードコードしなくてよくなりますね。

### `AssetBundle`

デモが動作するために必要なファイルをバケットに置くカスタムリソースです。

どうやって全自動でこれを置くか？　と思案した結果、下のような流れになりました。

1. `ng build --prod` でクライアントのコードをバンドルする
1. `serverless-webpack` と `copy-webpack-plugin` を使い、クライアントのアセットバンドルを Lambda のバンドルの中に含める
1. カスタムリソースでそれをバケットに置く

`yarn run deploy` を叩くとこの流れで一切合切がバケットに上がります。

# その他

カスタムリソースを書くのが面倒すぎて、以前作った [cfn-custom-resource-helper](https://github.com/y13i/cfn-custom-resource-helper) という npm パッケージを使っています。これの紹介はまたどこかで。

# 最後に

遅刻すみませんでした。