こんにちは、広野です。
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 特有の内容ではないので、他の要件での参考にもなると思います。
本記事が皆様のお役に立てれば幸いです。