amazon-web-servicesamazon-s3amazon-cognitoamazon-iam

Cognito Identity Pools - Attribute-based access control with "dynamic" attributes


I have hundreds of S3 buckets and dozens of users in Cognito User Pool. I want to be able to select which user can access which S3 bucket, for example:

and so on.

I would love to be able to do it without creating a dedicated API creating a dynamic policies. I thought about utilising Cognito Identity Pools and Attribute-based access control.

There is a cool example where an user gets an attribute "department": "legal" and is then assigned a role that is allowed to query only the buckets with -legalsuffix, thanks to ${aws:PrincipalTag/department} magic.

If my users were to access only one bucket, that would be a solution. However, in my case a user could get assigned to dozens or hundreds of buckets (think "multiple departments" in the example from AWS docs).

I thought of using multiple custom attributes on each user:

and creating a policy that allows you to access given bucket_n if and only if you have an attribute bucket_n: true.

This would work if I had at most 50 buckets (the hard limit of Custom Attributes in Cognito).

In my case, this value is slightly higher (a couple hundreds). I can have users having access to 200+ buckets as well as ones being allowed to only one bucket.

Is there any way to achieve my goal with Cognito Identity Pools and IAM Policies?


Solution

  • Well, I have found a "workaround" that might be useful for some of you.

    First, go to Cognito User Pools and create a custom attribute of your choice that will hold all your values. Bear in mind that Cognito has 2048 character limit of an attribute value.

    Let's call that attribute attribute_groups. Put all your values there as a string with a delimiter of your choice. In my case user1 with attribute custom:attribute_groups = "123o789" would mean that user1 is logically in group 123 and 789.

    Map these attributes in Cognito Identity Pools. Following the example Tag key for principal: groups would be selected from Attribute name: custom:attribute_groups.

    Lastly, set up the IAM policy that your authenticated users in user pool assume as follows. In my case, I want to allow group 123 to list s3://my_bucket/123, 456 to list s3://my_bucket/456 and so on.

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "1",
                "Effect": "Allow",
                "Action": [
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws:s3:::my_bucket"
                ],
                "Condition": {
                    "StringLike": {
                        "s3:prefix": [
                            "123/*"
                        ],
                        "aws:PrincipalTag/groups": [
                            "*123*"
                        ]
                    }
                }
            },
            {
                "Sid": "2",
                "Effect": "Allow",
                "Action": [
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws:s3:::my_bucket"
                ],
                "Condition": {
                    "StringLike": {
                        "s3:prefix": [
                            "456/*"
                        ],
                        "aws:PrincipalTag/groups": [
                            "*456*"
                        ]
                    }
                }
            },
            {
                "Sid": "3",
                "Effect": "Allow",
                "Action": [
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws:s3:::my_bucket"
                ],
                "Condition": {
                    "StringLike": {
                        "s3:prefix": [
                            "789/*"
                        ],
                        "aws:PrincipalTag/groups": [
                            "*789*"
                        ]
                    }
                }
            }
        ]
    }
    

    Finishing my example - when the user1 attains their credentials (you can test that by creating the user, setting its attributes and then doing aws cognito-idp initiate-auth to get ID token, then aws cognito-identity get-id to get the identity and finally aws cognito-identity get-credentials-for-identity to get the access key id/secret access key/session token), they can perform aws s3 ls s3://my_bucket/123 and aws s3 ls s3://my_bucket/789.

    Unfortunately, this is still severely limited by:

    and with such granular IAM Policy you can have at most 15-20 "groups".

    You may somehow counter that by creating multiple Policies and multiple ("fake") Cognito User Pool Groups, have each user be attached to every group and then iterate over the groups them using cognito:roles attribute inside ID token and assume these roles using GetCredentialForIdentity that allows you to set CustomRoleArn. It works (I've checked), but is really hacky.

    However, there is a trick that might extend Cognito capabilities to some of you and get rid of that limit. The requirement is that the structure of directories is predictable and you don't need access to subdirectories. We will use the fact that ${s3:prefix} is also a variable.

    If so, your policy may look as follows:

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "1",
                "Effect": "Allow",
                "Action": [
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws:s3:::my_bucket"
                ],
                "Condition": {
                    "StringLike": {
                        "aws:PrincipalTag/groups": [
                            "*${s3:prefix}*"
                        ]
                    }
                }
            },
            {
                "Sid": "2",
                "Effect": "Allow",
                "Action": [
                    "s3:GetObject"
                ],
                "Resource": [
                    "arn:aws:s3:::my_bucket/${s3:prefix}/*"
                ],
                "Condition": {
                    "StringLike": {
                        "aws:PrincipalTag/groups": [
                            "*${s3:prefix}*"
                        ]
                    }
                }
            }
        ]
    }
    

    And then in custom:attribute_groups you have to specify all prefixes (i.e. all directories) that given user has access to. Note that they will not be allowed to access subdirectories of these directories, as then the ${s3:prefix} will be different.

    For example, an user with:

    custom:attribute_groups = directoryA####directoryA/subdirectoryAA####directoryC/subdirectoryCA/subsubdirectoryCAA

    would be allowed to access files in:

    And no other (sub)directories! directoryA/subdirectoryAB is unreachable, even though directoryA is.

    Of course, each attribute can hold maximum 2048 characters. But you can create up to 50 custom attributes in Cognito and extend your policy accordingly. I think that should be sufficient for most conventional use cases.