Storage Browser for Amazon S3 でアクセス可能なバケットを Amazon Cognito グループ単位で動的に変更する [前編]

こんにちは、広野です。

Storage Browser for Amazon S3 の記事が意図せずしてシリーズ化されてきました。使い始めると、いろいろとやりたいことができてしまい。紹介する内容はタイトルの通りなので省略します。内容が多いので、一旦前編としてアーキテクチャと Amazon Cognito 周りの実装を紹介します。後編は React アプリ側の実装です。

背景

Storage Browser for Amazon S3 は、皆さんが AWS マネジメントコンソールでバケットやオブジェクトを操作しているようなおなじみの UI を、自分が開発したアプリに組み込める画期的な UI モジュールです。

なんですが、基本的に、アクセス可能なバケットやフォルダは「固定」です。アプリ内で所定の設定を記述するのですが、ユーザーの属性によって動的にアクセス可能なバケットやフォルダを変えようとしたらどうすればいいのだろう?というのが起点でした。実際、そんなニーズが社内開発案件でありました。

実装したいと考えた仕様は、Amazon Cognito のユーザーをグループに所属させ、グループごとにアクセス可能なバケットを動的に変更したい、です。

しかし、公式ドキュメントを読んでも直接的に参考となるドキュメントはありませんでした。IAM Identity Center と S3 Access Grants を使用した構成の記述はありますが、そこまで大がかりにしたくありません。Amazon Cognito による制御にとどめたいです。また、ドキュメントにある Customer managed auth というカスタマイズ例を活かしてプログラマティックにアクセス可能なバケットを変えられると期待したのですが、どう書いてもうまくいきませんでした。(d.items is not iterable というエラーが出るだけ)

将来的には公式のコードサンプル提供や有志の方の検証により、よりスマートな方法が確立されると思いますが、今時点で私が実現できた方法を紹介します。

仕組みの概要

まず、ベースとなる AWS リソースのアーキテクチャは以下です。

  • Amazon Cognito ユーザーは、グループに所属させる。本記事では、1ユーザーにつき所属するグループは1つのみとする。
  • Amazon Cognito グループに、グループごとにアクセスを許可するリソースを設定した IAM ロールを関連付ける。
  • それにより、アプリで Amazon Cognito の認証を受けたユーザーは、所属する Cognito グループの IAM ロールを割り当てられる。
  • IAM ロールには、そのグループにアクセスを許可する Amazon S3 バケットへのアクセス権限を記述する。
  • アプリ側では、Amazon Cognito の認証を済ませることにより属性情報の1つとして所属する Cognito グループ名を取得できる。(後編記事で紹介)
  • Cognito グループ名から、ビジネスロジックに応じて Storage Browser for Amazon S3 でアクセスさせる S3 バケットを動的に設定するアプリコードを書く。(後編記事で紹介)

Amazon Cognito 側の実装

Amazon Cognito グループによって Cognito ユーザーに割り当てる IAM ロールを設定するには、Amazon Cognito ユーザープールに関連付けた Amazon Cognito ID プールが必要です。Cognito グループの設定はユーザープール内で設定するのですが、ID プールが存在することが前提で機能するので、注意しましょう。

設定方法は、AWS 公式ドキュメント通りです。

開発側の注意点として、Cognito グループごとに IAM ロールを作成しなければならないことです。理想は Cognito グループを変数として IAM ロールに記述できればよいのですが、散々調べましたがそのような機能は残念ながらありません。Cognito ユーザを変数にすることはできますが、今回の要件ではないので使用できません。

今回の要件では 1 ユーザーが所属するグループは 1 つのみとしていますが、実案件では複数のグループに所属することは多々あります。そのときに適用される IAM ロールはグループの優先順位の数値に従って決められたグループの IAM ロールになりますので、優先順位の設定に注意が必要です。

ここまでは Cognito ユーザープール側の設定です。

Cognito グループに IAM ロールを関連付けるときの設定として、Cognito ID プール側の設定があります。

ID プールでは、Cognito ユーザーに割り当てる IAM ロールとして標準的に 2 つの IAM ロールを設定します。認証されたロールとゲストロールです。下の画面のように作成、関連付けをしておく必要がありますが、今回の要件では Cognito グループに IAM ロールを関連付けるため、実際のところ使用する予定はないのですが、仕様的に必要になります。

さて、この使用しないと言った 2 つのロールですが、何もしなければこのロールが Cognito ユーザーに割り当てられてしまいます。Cognito グループに関連付けた IAM ロールを割り当てて欲しいので、そのような設定を明示的にする必要があります。

Cognito ID プールの設定画面で以下の設定を見つけます。ここで、トークンで preferred_role クレームを持つロールを選択する を必ず選択します。

preferred_role は何かというと、Amazon Cognito ユーザーの属性で、そのユーザーに割り当てる IAM ロールを個別に設定するものです。なので Cognito ユーザー単位に設定が可能なのですが、Cognito グループに所属し、かつ Cognito グループに IAM ロールが関連付けられている場合はその IAM ロールが preferred_role として設定されます。つまり、Cognito グループに関連付けられた IAM ロールを使用するにはこの設定を選択する必要があるということです。

そして、ロールの解決の設定は決めなのですが、もしユーザーが Cognito グループに所属していなかったらどうするか?の判断を設定することを意味しています。前述した、認証されたロールを使用させるか、IAM ロールを割り当てないか、です。プログラミングで言う、どの条件にも該当しないときにはこうする、的な処置だと思って下さい。

これら Cognito ID プールから割り当てる IAM ロールの記述は、共通して以下の AWS 公式ドキュメント通りに書きます。

具体的な設定は、次章の AWS CloudFormation テンプレートから読み取って頂けたらと思います。少々解説は入れます。

参考: AWS CloudFormation テンプレート

解説をインラインでコメントしておきます。

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates a Cognito user pool and a Cognito ID pool.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SubName:
    Type: String
    Description: System sub name of example. (e.g. prod or test)
    Default: test
    MaxLength: 10
    MinLength: 1

  DomainName:
    Type: String
    Description: Domain name for URL. xxxxx.xxx
    Default: sampledomain.com
    MaxLength: 40
    MinLength: 5

  SubDomainName:
    Type: String
    Description: Sub domain name for URL.
    Default: subdomain
    MaxLength: 20
    MinLength: 1

  SesId:
    Type: String
    Description: Amazon SES ID for sending emails. (email addreess or domain)
    Default: sampledomain.com
    MaxLength: 100
    MinLength: 5

  SesConfigurationSet:
    Type: String
    Description: Amazon SES configuration set for sending emails.
    MaxLength: 100
    MinLength: 5

  CognitoAdminAlias:
    Type: String
    Description: The alias name of Cognito Admin email address. (e.g. Admin)
    Default: Admin
    MaxLength: 100
    MinLength: 5

  CognitoReplyTo:
    Type: String
    Description: Cognito Reply-to email address. (e.g. xxx@xxx.xx)
    MaxLength: 100
    MinLength: 5

  CognitoEmailFrom:
    Type: String
    Description: Cognito e-mail from address. (e.g. xxx@xxx.xx)
    MaxLength: 100
    MinLength: 5

Resources:
# ------------------------------------------------------------#
# Cognito IdP Roles (IAM)
# ------------------------------------------------------------#
# 以下の IAM ロールは使用する想定のない「認証されたロール」なので、このサンプルでは設定が適当です。すみません。
  CognitoIdPAuthRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub example-CognitoIdPAuthRole-${SubName}
      Description: This role allows Cognito authenticated users to access AWS resources.
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: cognito-identity.amazonaws.com
            Action:
              "sts:AssumeRoleWithWebIdentity"
            Condition:
              StringEquals:
                "cognito-identity.amazonaws.com:aud": !Ref IdPool
              "ForAnyValue:StringLike":
                "cognito-identity.amazonaws.com:amr": authenticated
      Policies:
        - PolicyName: !Sub example-CognitoIdPAuthRolePolicy-${SubName}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "lambda:InvokeFunctionUrl"
                Resource:
                  - Fn::ImportValue:
                      !Sub example-${SubName}-LambdaBedrockArn
                  - Fn::ImportValue:
                      !Sub example-${SubName}-LambdaBedrockAgentArn
                Condition:
                  StringEquals:
                    "lambda:FunctionUrlAuthType": AWS_IAM
# 以下の IAM ロールは使用する想定のない「ゲストロール」なので、このサンプルでは設定が適当です。すみません。
  CognitoIdPUnauthRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub example-CognitoIdPUnauthRole-${SubName}
      Description: This role allows Cognito unauthenticated users to access AWS resources.
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: cognito-identity.amazonaws.com
            Action:
              "sts:AssumeRoleWithWebIdentity"
            Condition:
              StringEquals:
                "cognito-identity.amazonaws.com:aud": !Ref IdPool
              "ForAnyValue:StringLike":
                "cognito-identity.amazonaws.com:amr": unauthenticated
      Policies:
        - PolicyName: !Sub example-CognitoIdPUnauthRolePolicy-${SubName}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
                Action:
                  - "s3:ListBucket"
                Resource:
                  - !Sub "arn:aws:s3:::example-${SubName}-amplifystorage"
                Effect: Allow
# 以下の IAM ロールが Cognito グループに割り当てるものなので意味があります。特定の S3 バケットアクセスを許可しています。
  CognitoGroupBasicRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub example-CognitoGroupBasicRole-${SubName}
      Description: This role allows Cognito authenticated users that belong to BASIC group to access AWS resources.
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: cognito-identity.amazonaws.com
            Action:
              "sts:AssumeRoleWithWebIdentity"
            Condition:
              StringEquals:
                "cognito-identity.amazonaws.com:aud": !Ref IdPool
              "ForAnyValue:StringLike":
                "cognito-identity.amazonaws.com:amr": authenticated
      Policies:
        - PolicyName: !Sub example-CognitoGroupBasicPolicy-${SubName}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - "s3:ListBucket"
                Resource:
                  - !Sub "arn:aws:s3:::example-${SubName}-kbdatasource"
                Effect: Allow
              - Action:
                  - "s3:DeleteObject"
                  - "s3:PutObject"
                Resource:
                  - !Sub "arn:aws:s3:::example-${SubName}-kbdatasource/*"
                Effect: Allow

# ------------------------------------------------------------#
# Cognito
# ------------------------------------------------------------#
# Cognito ユーザープールについては、特定の仕様で作成しています。ここでは言及しません。
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub example-${SubName}
      MfaConfiguration: "ON"
      EnabledMfas:
        - SOFTWARE_TOKEN_MFA
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireUppercase: true
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
          TemporaryPasswordValidityDays: 180
      AccountRecoverySetting:
        RecoveryMechanisms:
          - Name: verified_email
            Priority: 1
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
      AutoVerifiedAttributes:
        - email
      DeviceConfiguration:
        ChallengeRequiredOnNewDevice: false
        DeviceOnlyRememberedOnUserPrompt: false
      EmailConfiguration:
        ConfigurationSet: !Ref SesConfigurationSet
        EmailSendingAccount: DEVELOPER
        From: !Sub "${CognitoAdminAlias} <${CognitoEmailFrom}>"
        ReplyToEmailAddress: !Ref CognitoReplyTo
        SourceArn: !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/${SesId}
      EmailVerificationMessage: !Sub "example-${SubName} Verification code: {####}"
      EmailVerificationSubject: !Sub "example-${SubName} Verification code"
      UsernameAttributes:
        - email
      UsernameConfiguration:
        CaseSensitive: false
      UserPoolAddOns:
        AdvancedSecurityMode: "OFF"
      UserPoolTags:
        Cost: !Sub example-${SubName}
  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      UserPoolId: !Ref UserPool
      ClientName: !Sub example-${SubName}-appclient
      GenerateSecret: false
      RefreshTokenValidity: 3
      AccessTokenValidity: 6
      IdTokenValidity: 6
      ExplicitAuthFlows:
        - ALLOW_USER_SRP_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH
      PreventUserExistenceErrors: ENABLED
      SupportedIdentityProviders: 
        - COGNITO
      CallbackURLs:
        - !Sub https://${SubDomainName}.${DomainName}/index.html
      LogoutURLs:
        - !Sub https://${SubDomainName}.${DomainName}/index.html
      DefaultRedirectURI: !Sub https://${SubDomainName}.${DomainName}/index.html
      AllowedOAuthFlows:
        - implicit
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthScopes:
        - email
        - openid

# ここで、BASIC という名前の Cognito ユーザーグループと、割り当てる IAM ロールを指定しています。
  UserPoolGroupBasic:
    Type: AWS::Cognito::UserPoolGroup
    Properties:
      Description: example User Group which allows users able to access basic contents.
      GroupName: BASIC
      Precedence: 101
      UserPoolId: !Ref UserPool
      RoleArn: !GetAtt CognitoGroupBasicRole.Arn

  UserPoolGroupAdmin:
    Type: AWS::Cognito::UserPoolGroup
    Properties:
      Description: example User Group which allows users able to access management tools.
      GroupName: ADMIN
      Precedence: 1
      UserPoolId: !Ref UserPool

# Cognito ID プールの設定です。
  IdPool:
    Type: AWS::Cognito::IdentityPool
    Properties:
      IdentityPoolName: !Sub example-${SubName}
# ここで、クラシックフローを無効にする設定を入れています。有効になっていると、IAM ロールの割り当てが利かないかもしれません。
      AllowClassicFlow: false
      AllowUnauthenticatedIdentities: false
      CognitoIdentityProviders:
        - ClientId: !Ref UserPoolClient
          ProviderName: !GetAtt UserPool.ProviderName
          ServerSideTokenCheck: true
      IdentityPoolTags: 
        - Key: Cost
          Value: !Sub example-${SubName}
  IdPoolRoleAttachment:
    Type: AWS::Cognito::IdentityPoolRoleAttachment
    Properties:
      IdentityPoolId: !Ref IdPool
      Roles:
        authenticated: !GetAtt CognitoIdPAuthRole.Arn
        unauthenticated: !GetAtt CognitoIdPUnauthRole.Arn
# ここで、Cognito グループに応じて IAM ロールを割り当てるという設定を入れています。
      RoleMappings:
        # CloudFormation で設定するときには、以下の userpool: という項目を任意の名前でいいので差し込まないとエラーになります。
        # 設定を複数持てるので、以下の単位で追加できます。IDプロバイダーを Cognito 以外にソーシャルプロバイダーも指定できます。
        userpool:
          IdentityProvider: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}:${UserPoolClient}
          Type: Token
          AmbiguousRoleResolution: AuthenticatedRole

 

以上で、前編の Amazon Cognito の実装を終了します。後編では React のコード実装例を紹介します。

まとめ

いかがでしたでしょうか。

本記事では、Storage Browser for Amazon S3 の利用で必要となる Amazon Cognito ID プールの実装を中心に説明しました。Storage Browser 特有の内容ではないので、他の要件での参考にもなると思います。

本記事が皆様のお役に立てれば幸いです。

関連記事

【re:Invent 2024 発表】Storage Browser for Amazon S3 を React アプリに組み込みました
2024/12/1 に GA されました Storage Browser for Amazon S3 を既存 の S3 と React アプリに組み込みました。(AWS Amplify, Next.js 不使用)
Storage Browser for Amazon S3 でダウンロードを無効にする
デフォルトの設定ではセキュリティ的に実用的ではなかったので、少々セキュリティ設定を組み込んでみました。
Storage Browser for Amazon S3 のアクセスログを取得・検索する [AWS CloudTrail 利用]
Storage Browser を使用する上で絶対に必要になる、アクセスログを取得する設定を入れました。
Storage Browser for Amazon S3 でアクセス可能なバケットを Amazon Cognito グループ単位で動的に変更する [後編]
後編では React アプリ側の設定、コードを中心に説明します。
著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもフロントエンド開発の方が好きでして、バックエンド構築を簡単にしてくれたAWSには感謝の気持ちの方が強いです。
取得資格:AWS 認定は13資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2024 Japan AWS Top Engineer 受賞
2022 - 2024 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWSアプリケーション開発クラウドソリューション
シェアする
タイトルとURLをコピーしました