Cognito認証によるDynamoDB FGAC #アドカレ2015

2015.12.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

丹内です。
昨日はGoogleアカウントでCognito認証を行いました。
本日はその応用として、DynamoDBのアクセス制御をご紹介します。

前提

AndroidアプリでWebアイデンティティフェデレーションを使ったCognito認証ができていること

DynamoDB FGACとは

IAMポリシー変数でCondition節を記述したIAM PolicyをCognito認証して引き受けるAssume Roleにアタッチすることで、Cognito Identity Pool或いはCognito Identity IDごとなど様々な権限に応じて操作可能なDynamoDBの項目を細かく設定できる機能です。
以下に手順を示します。

DynamoDBテーブルを作成する

今回は例として、以下の様なテーブルを作成しました。個別の項目には、特に意味はありません。

  • Table Name: projects
  • Hash Key: name(String)
  • Range Key: member(String)
  • Attribute: description(String)

FGAC用IAMポリシーを作成し、Cognito Auth Roleにアタッチする

以下のポリシーを作成し、Cognito認証に成功した際にアクセス可能なポリシーを引き受けられるようにアタッチします。
この例では、ユーザは自分のCognito Identity IDと同じHash KeyしかQueryできません。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:Query"
            ],
            "Resource": [
                "arn:aws:dynamodb:ap-northeast-1:<Account ID>:table/projects",
                "arn:aws:dynamodb:ap-northeast-1:<Account ID>:table/projects/index/*"
            ],
            "Condition": {
                "ForAllValues:StringEquals": {
                    "dynamodb:LeadingKeys": [
                        "${cognito-identity.amazonaws.com:sub}"
                    ]
                }
            }
        }
    ]
}

cognito-identity.amazonaws.com:subでIdentity ID単位、cognito-identity.amazonaws.com:audでIdentity Pool単位で設定できます。

マッピングクラスを実装する

ここからはAndroidアプリです。まずはマッピングクラスを作成します。

@DynamoDBTable(tableName = "projects")
public class Project {
    private String name;
    private String member;
    private String description;

    @DynamoDBHashKey(attributeName = "name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @DynamoDBRangeKey(attributeName = "member")
    public String getMember() {
        return member;
    }

    public void setMember(String member) {
        this.member = member;
    }

    @DynamoDBAttribute(attributeName = "description")
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

DynamoDBの処理を実装する

昨日のエントリに加えて、DynamoDBにQueryを実施する処理を追加します。

private void handleSignInResult(GoogleSignInResult result) {
        Log.d(TAG, "handleSignInResult:" + result.isSuccess());
        if (result.isSuccess()) {
            GoogleSignInAccount account = result.getSignInAccount();
            Log.d(TAG, "IdToken: " + account.getIdToken());

            googleAuthenticationStream(account)
                    .flatMap(this::cognitoAuthenticationStream)
                    .flatMap(this::ddbProjectStream)
                    .subscribeOn(Schedulers.newThread())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(
                            ddbResult -> Toast.makeText(getActivity(), ddbResult.toString(), Toast.LENGTH_SHORT).show(),
                            Throwable::printStackTrace,
                            () -> Log.d(TAG, "onCompleted")
                    );
        }
}

Observable<PaginatedQueryList<Project>> ddbProjectStream(final CognitoCachingCredentialsProvider provider) {
    return Observable.create(subscriber -> {
        AmazonDynamoDBClient ddbClient = new AmazonDynamoDBClient(provider);
        DynamoDBMapper mapper = new DynamoDBMapper(ddbClient);
        ddbClient.setRegion(Region.getRegion(Regions.AP_NORTHEAST_1));

        Project projectToFind = new Project();
        projectToFind.setName(provider.getCachedIdentityId());

        String queryString = "tannai";

        com.amazonaws.services.dynamodbv2.model.Condition rangeKeyCondition = new com.amazonaws.services.dynamodbv2.model.Condition()
                .withComparisonOperator(ComparisonOperator.EQ.toString())
                .withAttributeValueList(new AttributeValue().withS(queryString));

        DynamoDBQueryExpression<Project> queryExpression = new DynamoDBQueryExpression<Project>()
                .withHashKeyValues(projectToFind)
                .withRangeKeyCondition("member", rangeKeyCondition);

        PaginatedQueryList<Project> result = mapper.query(Project.class, queryExpression);

        subscriber.onNext(result);
    });
}

動作確認

projectToFind.setName(provider.getCachedIdentityId())でHash Keyを、withAttributeValueList(new AttributeValue().withS(queryString))でRange Keyを設定します。
事前にマネジメントコンソールからCognito Identity IDを調べておき、適当なRange Keyと共にDynamoDBにItemを作成します。
このItemに対するQueryは成功します。自分のIdentity ID以外のHash Keyでは、AccessDeniedExceptionで失敗します。

まとめ

Cognitoを使うことで、AWSリソースに簡単にアクセスできるのはとても良いです。
また、私は実務ではサーバサイドしかやっていませんが、サーバサイドアプリケーションを書く人こそ、クライアントからAWSを使うことで得られる知識(IAMやCognito)も大きいと思います。
明日からぜひ使ってみてください!

参考リンク