行レベルセキュリティーを考慮したQuickSightの登録済みユーザーの埋め込みダッシュボード機能を利用する

# はじめに
[前回この記事](https://qiita.com/karuakun/items/128c4c13e8e28a598432)でQuickSightのRLSを考慮した匿名ユーザー向けの埋め込みダッシュボードの表示について説明しましたが、登録済みユーザーのダッシュボードも試したので記録として残しておきます。

[前編](https://qiita.com/karuakun/items/128c4c13e8e28a598432)及び[後編](https://qiita.com/karuakun/items/bc27dec079696ed68d01)で作成したダッシュボードやソースをもとに設定を追加していくので、実際に手を動かして確認する場合はまず[前編](https://qiita.com/karuakun/items/128c4c13e8e28a598432)及び[後編](https://qiita.com/karuakun/items/bc27dec079696ed68d01)を確認してみてください。

# 匿名ユーザーと登録済みユーザーの埋め込みダッシュボードの違い

前回作成した匿名ユーザーの埋め込みダッシュボードと、登録済みユーザーの埋め込みダッシュボードには次のような違いがあります。

登録済みユーザーの埋め込みダッシュボードは、

- EnterpriseEditionは必須だが、セッションキャパシティープランでなくても利用できる
- ダッシュボード表示時にQuickSightのReader以上のアカウントを割り当てる必要がある
- セッションタグを利用した行レベルセキュリティー(RLS)は利用できないため、事前にユーザーごとのRLSを設定する必要がある

やはり一番の違いはセッションキャパシティープランでなくても利用できる点だと思います。セッションキャパシティープランを必要とする匿名ユーザーの埋め込みダッシュボードでは開いたセッション数で課金されるのに対し、登録済みユーザーの埋め込みダッシュボードでは登録されたユーザーの数で課金されます。

[QuickSight 料金](https://aws.amazon.com/jp/quicksight/pricing/)

このため、ダッシュボードの利用人数は少ないんだけれど特定の人が良く見るとか、一人当たりのダッシュボード表示回数はあまりないんだけれどとにかく利用者数が多いといった埋め込みダッシュボードの表示パターンをもとにQuickSightの契約プランを検討する必要があります。

また、いつRLSを割り当てるかも重要な観点です。登録済みユーザーの埋め込みダッシュボードではセッションタグが利用できないため、RLSは事前に登録しておく必要があります。匿名ダッシュボードのようにダッシュボードURL発行時に割り当てるRLSを制御できない点も注意が必要です。

# グループの追加

匿名ユーザーの埋め込みダッシュボードの場合、作成したダッシュボードはユーザーかグループに対して事前に共有しておく必要があります。ユーザーごとにダッシュボードを共有するとユーザー登録が煩雑になるため、ダッシュボードをグループに共有しそのグループにユーザーを登録してあげたほうが運用しやすいと思います。グループはUIからは追加できないのでAWS CLIから登録する必要があります。

``` sh
aws quicksight create-group --aws-account-id $awsAccountId --namespace default --group-name SampleDashboard
```

``` json
{
  "Status": 201,
  "Group": {
    "Arn": "arn:aws:quicksight:ap-northeast-1:xxxxxxxxxxxx:group/default/SampleDashboard",
    "GroupName": "SampleDashboard",
    "PrincipalId": "group/d-xxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "RequestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

# ダッシュボードをグループに公開

埋め込みたいダッシュボードを先ほど作成したグループに割り当てていきます。ここはUIからやったほうが分かりやすいのでUIで操作します。

`ダッシュボードの共有`メニューから
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/9d221b12-6894-fcc5-7412-1279efc50adf.png)
共有したいグループを検索して共有ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/87becec5-56e4-b504-1514-d04336a6ff2f.png)
ダッシュボードアクセスの管理ボタンをクリックすると、現在共有されているユーザー、もしくはグループの一覧を確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/5790541b-7a4f-2d9b-fefe-e4732c146924.png)

# 登録済みユーザーの埋め込みダッシュボードの表示

それではダッシュボードを表示していきましょう。

まずはダッシュボードを表示するためのユーザーを登録し、先ほどダッシュボードを公開したグループに所属させます。
ユーザーの登録やグループへの参加はあらかじめCLIから設定しておいてもよいのですが、アプリ側でダッシュボードURLの作成時にQuickSightのユーザーを登録してグループに参加させたほうが運用が楽になるケースが多いと思うので、ここからはアプリ側で実装していきます。

前回使った画面に登録済みユーザーでダッシュボードを表示するためのボタンを一つ配置しておきます。

``` diff_html:Index.cshtml
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <form method="post">
        <div><select asp-for="SelectedDashboard" asp-items="Model.DashboardList" name="dashboard" id="dashboard"></select></div>
        <div><input type="submit" value="表示(User)" asp-page-handler="ViewDashboardForUser"/></div>
        <div><input type="submit" value="表示(Admin)" asp-page-handler="ViewDashboardForAdmin"/></div>
+       <div><input type="submit" value="表示(test1ユーザー)" asp-page-handler="ViewDashboardForTest1User"/></div>
    </form>
    @if (!string.IsNullOrEmpty(Model.EmbedDashboardUrl))
    {
        <div style="width: 100%">
            <iframe style="width: 100%; height: 600px" src="@Model.EmbedDashboardUrl"></iframe>
        </div>
    }
</div>
```
このボタンを押されたときに実施するのは次の3つです。

- 1.ユーザーを取得（いなかったら登録）
- 2.ユーザーをグループに追加
- 3.埋め込みダッシュボードのURLを取得してページのEmbedDashboardUrlプロパティーに設定

ここでは固定で`test1@example.com`というユーザーをQuickSightのユーザーとして作成していますが、実際はアプリ側で認証したユーザーのメールアドレスなどを使うことになると思います（アプリ側ではここで現在のユーザーが埋め込みダッシュボードを表示できるかのチェックを行ってもよいですね）。

``` cs:Index.cshtml.cs
public async Task OnPostViewDashboardForTest1User(string dashboard)
{
    const string userName = "test1@example.com";
    // STSからAWSのアカウントIDを取得し
    var account = await _amazonSecurityTokenService.GetCallerIdentityAsync(new GetCallerIdentityRequest());
    await LoadDashBoardsAsync(account.Account);

    // 1.ユーザーを取得（いなかったら登録）
    var quickSightUserArn = await GetUserArnAsync(account.Account, "default", userName);
    if (string.IsNullOrEmpty(quickSightUserArn))
    {
        quickSightUserArn = await RegisterUserAsync(account.Account, "default", userName, UserRole.READER);
    }

    // 2.ユーザーをグループに追加
    await _amazonQuickSight.CreateGroupMembershipAsync(
        new CreateGroupMembershipRequest
        {
            AwsAccountId = account.Account,
            GroupName = "SampleDashboard",
            Namespace = "default",
            MemberName = userName
        });

    // 3.埋め込みダッシュボードのURLを取得
    var response = await _amazonQuickSight.GenerateEmbedUrlForRegisteredUserAsync(new GenerateEmbedUrlForRegisteredUserRequest
    {
        AwsAccountId = account.Account,
        ExperienceConfiguration = new RegisteredUserEmbeddingExperienceConfiguration
        {
            Dashboard = new RegisteredUserDashboardEmbeddingConfiguration
            {
                InitialDashboardId = dashboard,
            }
        },
        UserArn = quickSightUserArn,
        SessionLifetimeInMinutes = 15,
    });
    EmbedDashboardUrl = response.EmbedUrl;
}
```

ユーザーの存在を確認するにはユーザー一覧を取得するAPI(`ListUserAsync`)を呼んでもいいのですが、ユーザーでフィルターするインターフェイスが無いので全ユーザーをロードしてしまいます。ここではユーザーが存在するかはユーザーの詳細表示API（`DescribeUserAsync`）を呼び出して、`ResourceNotFoundException`が発生したかどうかで判断しています。

``` cs
public async Task<string> GetUserArnAsync(string awsAccountId, string @namespace, string userName)
{
    try
    {
        var user = await _amazonQuickSight.DescribeUserAsync(new DescribeUserRequest
        {
            AwsAccountId = awsAccountId,
            Namespace = @namespace,
            UserName = userName
        });
        return user.User.Arn;
    }
    catch (ResourceNotFoundException)
    {
        return null;
    }
}

public async Task<string> RegisterUserAsync(string awsAccountId, string @namespace, string userName, UserRole userRole)
{
    var registerUserResponse = await _amazonQuickSight.RegisterUserAsync(new RegisterUserRequest
    {
        AwsAccountId = awsAccountId,
        Namespace = @namespace,
        IdentityType = IdentityType.QUICKSIGHT,
        Email = userName,
        UserName = userName,
        UserRole = userRole
    });
    return registerUserResponse.User.Arn;
}
```

さっそく実行してあげましょう。ローカルで実行する場合は[ngrokでブリッジしQuickSightにドメインを登録する](https://qiita.com/karuakun/items/bc27dec079696ed68d01#%E5%9F%8B%E3%82%81%E8%BE%BC%E3%81%BF%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%AE%E4%BA%8B%E5%89%8D%E8%A8%AD%E5%AE%9A)のを忘れないでください。

... おや？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/78fd8bb3-3e6b-c4de-951a-ad1b31ab357c.png)

あっ！肝心なRLSを追加するのを忘れていましたね。
RLSをCSVで設定している場合、新しいファイルをアップロードして更新する必要があるので`新しいデータセット`からデータセットを追加していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/2065a1bc-f626-4b50-c06f-825c7b7ded24.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/98d6f563-efd8-2294-7d07-d55239375f09.png)

今回はもとからいる`qsadmin1`に加え、今回アプリ側で利用する`test1@example.com`ユーザーを追加します。

``` csv:bi_sample_dashboard_permission.csv
UserName,role
qsadmin1,"user,admin"
test1@example.com,user
```
無事表示されましたね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/01e4aa02-bda6-3012-4fe8-72a7a6c07cff.png)

# まとめ

- 匿名ユーザーを利用するか登録済みユーザーを利用するかはダッシュボードの組み込み要件で決定する
- あらかじめグループを作成し、ダッシュボードをグループに対して共有する
- QuickSightユーザーはダッシュボードURL取得時に確認し、いなかったら登録してダッシュボードを共有したグループに所属させる