Writing IAM Policies: Grant Access to User-Specific Folders in an Amazon S3 Bucket

Many of you have asked how to construct an AWS Identity and Access Management (IAM) policy with folder-level permissions for Amazon S3 buckets. This week's guest blogger Elliot Yamaguchi, Technical Writer on the IAM team, will explain the basics of writing that type of policy.

______________________________________________________________________________________

To show you how to create a policy with folder-level permissions, I'll walk through a scenario similar to what many people have done on existing files shares, where every user has access to only his or her own home folder . With folder-level permissions, you can granularly control who has access to which objects in a specific bucket.

I'll show you a policy that grants IAM users access to the same Amazon S3 bucket so that they can use the AWS Management Console to store their information. All users will be able to upload or download files from their own folders, but they will not be able to access anyone else's folder in the bucket. 

After I explain the policy, I'll show you how to use policy variables so that you can use a single policy for an IAM group instead of creating an individual policy for each IAM user.

Throughout the rest of this post, I'll show and explain the policy, which will be associated with an IAM user named David. Also, I've already created a bucket named my-company with the following structure:

/home/Adele/
/home/Bob/
/home/David/
/restricted/
/root-file.txt

A brief lesson about Amazon S3 objects

Before I show and explain the policy, I'll need to review how Amazon S3 objects are named. This brief description isn't comprehensive, but it'll help you understand how the policy works. If you already know about Amazon S3 objects and prefixes, skip ahead to David's policy, below.

Amazon S3 stores data in a flat structure; you create a bucket, and the bucket stores objects. Amazon S3 doesn't have a hierarchy of sub-buckets or folders; however, tools like the AWS Management Console can emulate a folder hierarchy to present folders in a bucket by using the names of objects (also known as keys). For simplicity, you can think of an object's name as the full path of a file in a traditional file system. To give you an example, for an object that's named home/common/shared.txt, the console will show the shared.txt file in the common folder that is in the home folder. The names of these folders (such as home/ or home/common/) are called prefixes, and prefixes like these are what I use to specify David's home folder in his policy.

By the way, the slash (/) in a prefix like home/ isn't a reserved character—you could name an object (using the Amazon S3 API) with prefixes like home:common:shared.txt or home-common-shared.txt. However, the convention is to use a slash as the delimiter, and the Amazon S3 console (but not Amazon S3 itself) treats the slash as a special character for showing objects in folders.

David's policy

I'll start by showing you David's complete policy that I've associated with an IAM user named David by using the IAM console. Then, I'll break down the policy and explain how it works.

{
 "Version":"2012-10-17",
 "Statement": [
   {
     "Sid": "AllowUserToSeeBucketListInTheConsole",
     "Action": ["s3:ListAllMyBuckets", "s3:GetBucketLocation"],
     "Effect": "Allow",
     "Resource": ["arn:aws:s3:::*"]
   },
  {
     "Sid": "AllowRootAndHomeListingOfCompanyBucket",
     "Action": ["s3:ListBucket"],
     "Effect": "Allow",
     "Resource": ["arn:aws:s3:::my-company"],
     "Condition":{"StringEquals":{"s3:prefix":["","home/"],"s3:delimiter":["/"]}}
    },
   {
     "Sid": "AllowListingOfUserFolder",
     "Action": ["s3:ListBucket"],
     "Effect": "Allow",
     "Resource": ["arn:aws:s3:::my-company"],
     "Condition":{"StringLike":{"s3:prefix":["home/David/*"]}}
   },
   {
     "Sid": "AllowAllS3ActionsInUserFolder",
     "Effect": "Allow",
     "Action": ["s3:*"],
     "Resource": ["arn:aws:s3:::my-company/home/David/*"]
   }
 ]
}

This policy grants David full console access to only his folder (/home/David) and no one else's. While you could simply grant each user access to his or her own bucket, keep in mind that an AWS account can have up to 100 buckets by default. By creating home folders and granting the appropriate permissions, you can instead have hundreds of users share a single bucket. 

David's policy consists of four blocks; let's take a look at each individually.

Block 1: Allow required Amazon S3 console permissions

Before I begin identifying the specific folders David can have access to, I have to give him two permissions that are required for Amazon S3 console access: ListAllMyBuckets and GetBucketLocation.

   {
      "Sid": "AllowUserToSeeBucketListInTheConsole",
      "Action": ["s3:GetBucketLocation", "s3:ListAllMyBuckets"],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::*"]
   }

The ListAllMyBuckets action grants David permission to list all the buckets in the AWS account, which is required for navigating to buckets in the Amazon S3 console (and as an aside, you currently can't selectively filter out certain buckets, so users must have permission to list all buckets for console access). The console also does a GetBucketLocation call when users initially navigate to the Amazon S3 console, which is why David also requires permission for that action. Without these two actions, David will get an access denied error in the console.

Block 2: Allow listing objects in root and home folders

Although David should have access to only his home folder, he requires additional permissions so that he can navigate to his folder in the Amazon S3 console. David needs permission to list objects at the root level of the my-company bucket and in the home/ folder. The following policy grants these permissions to David:

   {
      "Sid": "AllowRootAndHomeListingOfCompanyBucket",
      "Action": ["s3:ListBucket"],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::my-company"],
      "Condition":{"StringEquals":{"s3:prefix":["","home/"],"s3:delimiter":["/"]}}
   }

Without the ListBucket permission, David can't navigate to his folder because he won't have permissions to view the contents of the root and home folders. When David tries to use the console to view the contents of the my-company bucket, the console will return an access denied error. Although this policy grants David permission to list all objects in the root and home folders, he won't be able to view the contents of any files or folders except his own (I specify these permissions in the next block).

This block includes conditions, which let you limit when a request to AWS is valid. In this case, David can list objects in the my-company bucket only when he requests objects without a prefix (objects at the root level) and objects with the home/ prefix (objects in the home folder). If David tried to navigate to other folders, such as restricted/, David is denied access.

To set these root and home folder permissions, I used two conditions: s3:prefix and s3:delimiter. The s3:prefix condition specifies the folders that David has ListBucket permissions for. For example, David can list all of the following files and folders in the my-company bucket:

/root-file.txt
/restricted/
/home/Adele/
/home/Bob/
/home/David/

But, David cannot list files or subfolders in the restricted/, home/Adele, or home/Bob folders.

Although the s3:delimiter condition isn't required for console access, it's still a good practice to include it in case David makes requests by using the API or command line interface (CLI). As previously noted, the delimiter is a character, such as a slash (/), that identifies the folder that an object is in. The delimiter is useful when you want to list objects as if they were in a file system. For example, let's assume the my-company bucket stored thousands of objects. If David includes the delimiter in his requests, he can limit the number of returned objects to just the names of files and subfolders in the folder he specified. Without the delimiter, in addition to every file in the folder he specified, David would get all files in any subfolders, all files in any sub-subfolder, etc.

Block 3: Allow listing objects in David's folder

In addition to the root and home folders, David requires access to all objects in the home/David/ folder and any subfolders that he might create. Here's a policy that allows this:

    {
      "Sid": "AllowListingOfUserFolder",
      "Action": ["s3:ListBucket"],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::my-company"],
      "Condition":{"StringLike":{"s3:prefix":["home/David/*"]}}
    }

In the condition, I use a StringLike expression in combination with the asterisk (*) to represent any object in David's folder, where the asterisk acts like a wildcard. That way, David can list any files and folders in his folder (home/David/). I couldn't include this condition in the previous block (AllowRootAndHomeListingOfCompanyBucket) because the previous block used the StringEquals expression, which would literally interpret the asterisk (*) as an asterisk (not as a wildcard).

In the next section, the AllowAllS3ActionsInUserFolder block, you'll see that the Resource element specifies my-company/home/David/*, which looks similar to the condition that I specified in this section. You might think that you can similarly use the Resource element to specify David's folder in this block. However, the ListBucket action is a bucket-level operation, meaning the Resource element for the ListBucket action applies only to bucket names and won't take into account any folder names. So, to limit actions at the object level (files and folders), you must use conditions.

Block 4: Allow all Amazon S3 actions in David's folder

Finally, I specify David's actions (such as read, write, and delete permissions) and limit them to just his home folder, as shown in the following policy:

    {
      "Sid": "AllowAllS3ActionsInUserFolder",
      "Effect": "Allow",
      "Action": ["s3:*"],
      "Resource": ["arn:aws:s3:::my-company/home/David/*"]
    }

For the Action element, I specified s3:*, which means David has permission to do all Amazon S3 actions. In the Resource element, I specified David's folder with an asterisk (*) (a wildcard) so that David can perform actions on the folder and inside the folder. For example, David has permission to change his folder's storage class, enable encryption, or make his folder public (perform actions against the folder itself). David also has permission to upload files, delete files, and create subfolders in his folder (perform actions in the folder).

An easier way to manage policies with policy variables

In David's folder-level policy I specified David's home folder. If you wanted a similar policy for users like Bob and Adele, you'd have to create separate policies that specify their home folders. Instead of creating individual policies for each user, you can use policy variables and create a single policy that applies to multiple users (a group policy). Policy variables act as placeholders. When you make a request to AWS, the placeholder is replaced by a value from the request when the policy is evaluated.

For example, I can use the previous policy and replace David's user name with a variable that uses the requester's user name (aws:username), as shown in the following policy. Also note that I declared the version number for both policies; while the version is optional for the previous policy, it's required whenever you use policy variables.

{
  "Version":"2012-10-17",
  "Statement": [
    {
      "Sid": "AllowGroupToSeeBucketListInTheConsole",
      "Action": ["s3:ListAllMyBuckets", "s3:GetBucketLocation"],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::*"]
    },
    {
      "Sid": "AllowRootAndHomeListingOfCompanyBucket",
      "Action": ["s3:ListBucket"],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::my-company"],
      "Condition":{"StringEquals":{"s3:prefix":["","home/"],"s3:delimiter":["/"]}}
    },
    {
      "Sid": "AllowListingOfUserFolder",
      "Action": ["s3:ListBucket"],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::my-company"],
      "Condition":{"StringLike":{"s3:prefix":["home/${aws:username}/*"]}}
    },
    {
       "Sid": "AllowAllS3ActionsInUserFolder",
       "Action":["s3:*"],
       "Effect":"Allow",
       "Resource": ["arn:aws:s3:::my-company/home/${aws:username}/*"]
    }
  ]
}

Whenever a user makes a request to AWS, the variable is replaced by the "friendly" user name of whomever made the request. So when David makes a request, ${aws:username} resolves to David; when Adele makes the request, ${aws:username} resolves to Adele, etc.

Here are some additional resources for learning about Amazon S3 folders and about IAM policies:

Comments