ASP.NET Coreでセキュリティーを考慮したQuickSightの匿名埋め込みダッシュボード機能を利用する（後編）

この記事はASP.NET Coreでセキュリティーを考慮したQuickSightの匿名埋め込みダッシュボード機能を利用するの後編にあたります。前編をご覧になっていない方は[こちら](https://qiita.com/karuakun/private/128c4c13e8e28a598432)から確認ください。

# ASP.NET Coreプロジェクトの作成

dotnet コマンドでプロジェクトを作成し、必要なライブラリを追加していきます。

``` sh
mkdir DashboardSample
cd DashboardSample
dotnet new webapp
dotnet add package AWSSDK.Extensions.NETCore.Setup
dotnet add package AWSSDK.QuickSight
dotnet add package AWSSDK.SecurityToken
```

アプリケーション初期化時に、SecurityTokenService(STS)とQuickSightをDIコンテナに追加します。

``` cs:Startup.cs
using Amazon.QuickSight;
using Amazon.SecurityToken;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DashboardSample
{
    public class Startup
    {
        // ... 略 ...
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
            services.AddAWSService<IAmazonQuickSight>();
            services.AddAWSService<IAmazonSecurityTokenService>();
            services.AddControllersWithViews();
        }
        // ... 略 ...
    }
}
```

# Viewの定義

埋込ダッシュボードを表示するためには、ダッシュボードのARNが必要になります。
ダッシュボード毎のARNを調べるのがめんどくさいので、一度ドロップダウンリストに現在のアカウントで利用できるダッシュボードの一覧を表示した後に、表示したいダッシュボードとダッシュボードを表示する権限を選択してダッシュボードを表示できるようにしておきます。

``` html
@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>
    </form>
    @if (!string.IsNullOrEmpty(Model.EmbedDashboardUrl))
    {
        <div style="width: 100%">
            <iframe style="width: 100%; height: 600px" src="@Model.EmbedDashboardUrl"></iframe>
        </div>
    }
</div>
```

ドロップダウンリストにダッシュボードの一覧が表示され、`表示(User)`ボタンと、`表示(Admin)`ボタンでそれぞれの権限のダッシュボードを表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/c9bef07e-e44b-ad58-72dc-13944123ccfb.png)

# ダッシュボード一覧の表示

ページの要求時は`SecurityTokenService`を利用して、現在ASP.NET Coreを動かしているAWSアカウントのAwsAccountIdを取得し、QuickSightの`ListDashboards`APIを使ってダッシュボードの一覧を取得しドロップダウンリストに表示するように変換しています。

``` cs:Index.cshtml.cs
        public async Task OnGet()
        {
            var account = await _amazonSecurityTokenService.GetCallerIdentityAsync(new GetCallerIdentityRequest());
            await LoadDashBoardsAsync(account.Account);
        }
        public async Task LoadDashBoardsAsync(string awsAccountId)
        {
            // ページングしてないので注意
            var response = await _amazonQuickSight.ListDashboardsAsync(new ListDashboardsRequest
            {
                AwsAccountId = awsAccountId
            });
            DashboardList = new SelectList(
                response.DashboardSummaryList,
                nameof(DashboardSummary.DashboardId),
                nameof(DashboardSummary.Name));
        }
```

## ダッシュボードの表示

`表示(User)`ボタンと、`表示(Admin)`ボタンクリック時にPost引数でわたってきたDashboardの値をもとにダッシュボードを表示していきます。

``` cs:Index.cshtml.cs
        // `表示(Admin)`ボタンクリック時の処理
        public async Task OnPostViewDashboardForAdmin(string dashboard)
        {
            var account = await _amazonSecurityTokenService.GetCallerIdentityAsync(new GetCallerIdentityRequest());
            await LoadDashBoardsAsync(account.Account);

            SelectedDashboard = dashboard;
            EmbedDashboardUrl = await GenerateEmbedUrlForAnonymousUserAsync(account.Account ,dashboard, "user,admin");
        }
        // `表示(User)`ボタンクリック時の処理
        public async Task OnPostViewDashboardForUser(string dashboard)
        {
            var account = await _amazonSecurityTokenService.GetCallerIdentityAsync(new GetCallerIdentityRequest());
            await LoadDashBoardsAsync(account.Account);

            SelectedDashboard = dashboard;
            EmbedDashboardUrl = await GenerateEmbedUrlForAnonymousUserAsync(account.Account, dashboard, "user");
        }
```


まずは、`DescribeDashboard`APIを使ってダッシュボードの詳細情報を取得します。

``` cs:GenerateEmbedUrlForAnonymousUserAsync()
        public async Task<string> GenerateEmbedUrlForAnonymousUserAsync(string awsAccountId, string dashboardId, string role)
        {
            var dashboard = await _amazonQuickSight.DescribeDashboardAsync(new DescribeDashboardRequest
            {
                AwsAccountId = awsAccountId,
                DashboardId = dashboardId
            });

            //... 略 ...
        }
```

`GenerateEmbedUrlForAnonymousUser`APIを使って先ほどのダッシュボードの詳細情報をもとに埋め込みダッシュボード用のURLを取得します。
SessionTags プロパティーに前編で設定した`tag_role`を指定しているところに注目してください。

``` cs:GenerateEmbedUrlForAnonymousUserAsync()
        public async Task<string> GenerateEmbedUrlForAnonymousUserAsync(string awsAccountId, string dashboardId, string role)
        {
            //... 略 ...

            var response = await _amazonQuickSight.GenerateEmbedUrlForAnonymousUserAsync(new GenerateEmbedUrlForAnonymousUserRequest
            {
                AwsAccountId = awsAccountId,
                Namespace = "default",
                AuthorizedResourceArns = new List<string>
                {
                    dashboard.Dashboard.Arn
                },
                ExperienceConfiguration = new AnonymousUserEmbeddingExperienceConfiguration
                {
                    Dashboard = new AnonymousUserDashboardEmbeddingConfiguration
                    {
                        InitialDashboardId = dashboardId,
                    }
                },
                SessionTags = new List<SessionTag>
                {
                    new() {Key = "tag_role", Value = role}
                },
                SessionLifetimeInMinutes = 60,
            });
        }
```

<details><summary>Index.cshtml.csのコード全体</summary><div>

``` cs:Index.cshtml.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Amazon.QuickSight;
using Amazon.QuickSight.Model;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Logging;

namespace DashboardSample.Pages
{
    public class IndexModel : PageModel
    {
        public SelectList DashboardList { get; set; }
        public string SelectedDashboard { get; set; }
        public string EmbedDashboardUrl { get; set; }

        private readonly ILogger<IndexModel> _logger;
        private readonly IAmazonSecurityTokenService _amazonSecurityTokenService;
        private readonly IAmazonQuickSight _amazonQuickSight;

        public IndexModel(
            ILogger<IndexModel> logger,
            IAmazonSecurityTokenService amazonSecurityTokenService,
            IAmazonQuickSight amazonQuickSight
            )
        {
            _logger = logger;
            _amazonSecurityTokenService = amazonSecurityTokenService;
            _amazonQuickSight = amazonQuickSight;
        }

        public async Task OnGet()
        {
            var account = await _amazonSecurityTokenService.GetCallerIdentityAsync(new GetCallerIdentityRequest());
            await LoadDashBoardsAsync(account.Account);
        }

        public async Task OnPostViewDashboardForAdmin(string dashboard)
        {
            var account = await _amazonSecurityTokenService.GetCallerIdentityAsync(new GetCallerIdentityRequest());
            await LoadDashBoardsAsync(account.Account);

            SelectedDashboard = dashboard;
            EmbedDashboardUrl = await GenerateEmbedUrlForAnonymousUserAsync(account.Account ,dashboard, "user,admin");
        }
        public async Task OnPostViewDashboardForUser(string dashboard)
        {
            var account = await _amazonSecurityTokenService.GetCallerIdentityAsync(new GetCallerIdentityRequest());
            await LoadDashBoardsAsync(account.Account);

            SelectedDashboard = dashboard;
            EmbedDashboardUrl = await GenerateEmbedUrlForAnonymousUserAsync(account.Account, dashboard, "user");
        }

        public async Task LoadDashBoardsAsync(string awsAccountId)
        {
            // ページングしてないので注意
            var response = await _amazonQuickSight.ListDashboardsAsync(new ListDashboardsRequest
            {
                AwsAccountId = awsAccountId
            });
            DashboardList = new SelectList(
                response.DashboardSummaryList,
                nameof(DashboardSummary.DashboardId),
                nameof(DashboardSummary.Name));
        }

        public async Task<string> GenerateEmbedUrlForAnonymousUserAsync(string awsAccountId, string dashboardId, string role)
        {
            var dashboard = await _amazonQuickSight.DescribeDashboardAsync(new DescribeDashboardRequest
            {
                AwsAccountId = awsAccountId,
                DashboardId = dashboardId
            });

            var response = await _amazonQuickSight.GenerateEmbedUrlForAnonymousUserAsync(new GenerateEmbedUrlForAnonymousUserRequest
            {
                AwsAccountId = awsAccountId,
                Namespace = "default",
                AuthorizedResourceArns = new List<string>
                {
                    dashboard.Dashboard.Arn
                },
                ExperienceConfiguration = new AnonymousUserEmbeddingExperienceConfiguration
                {
                    Dashboard = new AnonymousUserDashboardEmbeddingConfiguration
                    {
                        InitialDashboardId = dashboardId,
                    }
                },
                SessionTags = new List<SessionTag>
                {
                    new() {Key = "tag_role", Value = role}
                },
                SessionLifetimeInMinutes = 60,
            });
            return response.EmbedUrl;
        }
    }
}
```

</div></details>

早速実行してみると、、、ドロップダウンにダッシュボードの一覧は表示されたものの、ページには接続が拒否されましたと表示されてしまいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/919fd9aa-6939-fbea-0671-3af5e618c900.png)

# 埋め込みドメインの事前設定

QuickSightの埋め込みダッシュボード機能を利用する場合、どのドメインにダッシュボードを埋め込むかを[QuickSightの管理画面](https://ap-northeast-1.quicksight.aws.amazon.com/sn/admin#embedding)から設定する必要があります。ただし、ここでlocalhostやHTTPS以外から始まるドメインを指定すると次のようなメッセージが表示され設定することができません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/4ecba353-b7a1-3ff5-b14e-b0c4d41a6f06.png)

運用環境にデプロイすればよいのですが、確認するのに毎回デプロイするとなるとちょっと大変なので、ngrokを利用して外部のリバースプロキシ経由でデバックできるようにしましょう。
ngrokのインストールや簡単な使い方については、次の記事が参考になります。

[アプリをテストおよびデバッグするセットアップMicrosoft Teamsする](https://docs.microsoft.com/ja-jp/microsoftteams/platform/concepts/build-and-test/debug)

デバックサーバーが`https://localhost:44379/`で起動する場合は、次のようにngrokを起動します。

``` sh
> ngrok http -host-header=localhost https://localhost:44379/
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/93cc3e7f-8025-99b6-da78-92ed471c094d.png)
パブリックに公開された外部のURLであれば登録できます（ただし、無料のngrokだと定期的にURLが失効してしまうのでそのたびにQuickSightに登録する必要はあります）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/287ef21e-8a31-a17c-8b15-e9a844105614.png)

さて、改めて実行してみましょう。

`表示(User)`ボタンをクリックするとQuickSightのダッシュボードが表示され、ロールがuserのもののみ表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/cdf38042-2211-d2a3-3fbf-b6875e5038e5.png)
`表示(Admin)`ボタンをクリックするとadminとuserの両方が表示されていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/7cdc04ec-43d0-3a0b-814a-32f2c616ba6c.png)

# まとめ

- `AwsAccountId`は`SecurityTokenService`の`GetCallerIdentity`APIから取得するのが簡単
- 埋め込みURLは`GenerateEmbedUrlForAnonymousUser`APIで取得し、RLSで設定したタグ情報を引き渡すことでデータのセキュリティーを担保する
- 埋め込みダッシュボードを表示するにはあらかじめQuickSightにドメインの登録が必要、ローカルでデバックするときはngrokを利用すると簡単