|
| 1 | +export const meta = { |
| 2 | + title: 'Storage', |
| 3 | + description: 'Set up and connect to storage.' |
| 4 | +}; |
| 5 | + |
| 6 | +export function getStaticProps(context) { |
| 7 | + return { |
| 8 | + props: { |
| 9 | + meta |
| 10 | + } |
| 11 | + }; |
| 12 | +} |
| 13 | + |
| 14 | +<Callout warning> |
| 15 | + |
| 16 | +**Under active development:** The Storage experience for Amplify Gen 2 is under active development. The experience may change between versions of `@aws-amplify/backend`. Try it out and provide feedback at https://github.com/aws-amplify/amplify-backend/issues/new/choose |
| 17 | + |
| 18 | +</Callout> |
| 19 | + |
| 20 | +Adding storage to your Amplify backend enables uploading and downloading files. To get started using storage, create a file `amplify/storage/resource.ts`. Paste the following content into the file. |
| 21 | + |
| 22 | +```ts title="amplify/storage/resource.ts" |
| 23 | +import { defineStorage } from '@aws-amplify/backend'; |
| 24 | + |
| 25 | +export const storage = defineStorage({ |
| 26 | + name: 'myProjectFiles' |
| 27 | +}); |
| 28 | +``` |
| 29 | + |
| 30 | +Then include storage in your backend definition. |
| 31 | + |
| 32 | +```ts title="amplify/backend.ts" |
| 33 | +import { defineBackend } from '@aws-amplify/backend'; |
| 34 | +import { auth } from './auth/resource'; |
| 35 | +// highlight-next-line |
| 36 | +import { storage } from './storage/resource'; |
| 37 | + |
| 38 | +defineBackend({ |
| 39 | + auth, |
| 40 | + // highlight-next-line |
| 41 | + storage |
| 42 | +}); |
| 43 | +``` |
| 44 | + |
| 45 | +Now when you run `npx amplify sandbox` or deploy your app on Amplify, it will configure AWS resources for file upload and download. |
| 46 | + |
| 47 | +Before files can be accessed by your application, you must configure storage access rules. |
| 48 | + |
| 49 | +To learn how to use storage in your frontend, see docs on [uploading files](/javascript/build-a-backend/storage/upload/) or [downloading files](/javascript/build-a-backend/storage/download/). |
| 50 | + |
| 51 | +## Storage access |
| 52 | + |
| 53 | +By default, no users or other project resources have access to any files in storage. Access must be explicitly granted within `defineStorage` using the `access` callback. |
| 54 | + |
| 55 | +```ts title="amplify/storage/resource.ts" |
| 56 | +export const storage = defineStorage({ |
| 57 | + name: 'myProjectFiles', |
| 58 | + access: (allow) => ({ |
| 59 | + 'some/path/*': [ |
| 60 | + // access rules that apply to all files within "some/path/*" go here |
| 61 | + ], |
| 62 | + 'another/path/*': [ |
| 63 | + // access rules that apply to all files within "another/path/*" go here |
| 64 | + ] |
| 65 | + }) |
| 66 | +}); |
| 67 | +``` |
| 68 | + |
| 69 | +The access callback returns an object where each key in the object is a file prefix and each value in the object is a list of access rules that apply to that prefix. The following sections enumerate the types of access rules that can be applied. |
| 70 | + |
| 71 | +### Authenticated user access |
| 72 | + |
| 73 | +To grant all authenticated (signed in) users of your application read access to files that start with `foo/*`, use the following `access` configuration. |
| 74 | + |
| 75 | +Note that your backend must include `defineAuth` in order to use this access rule. |
| 76 | + |
| 77 | +```ts title="amplify/storage/resource.ts" |
| 78 | +export const storage = defineStorage({ |
| 79 | + name: 'myProjectFiles', |
| 80 | + access: (allow) => ({ |
| 81 | + 'foo/*': [allow.authenticated.to(['read'])] // additional actions such as "write" and "delete" can be specified depending on your use case |
| 82 | + }) |
| 83 | +}); |
| 84 | +``` |
| 85 | + |
| 86 | +### Guest user access |
| 87 | + |
| 88 | +To grant all guest (not signed in) users of your application read access to files that start with `foo/*`, use the following `access` config. |
| 89 | + |
| 90 | +Note that your backend must include `defineAuth` in order to use this access rule. |
| 91 | + |
| 92 | +```ts title="amplify/storage/resource.ts" |
| 93 | +export const storage = defineStorage({ |
| 94 | + name: 'myProjectFiles', |
| 95 | + access: (allow) => ({ |
| 96 | + 'foo/*': [allow.guest.to(['read'])] // additional actions such as "write" and "delete" can be specified depending on your use case |
| 97 | + }) |
| 98 | +}); |
| 99 | +``` |
| 100 | + |
| 101 | +### User group access |
| 102 | + |
| 103 | +If you have configured user groups in `defineAuth`, you can scope storage access to specific groups. Suppose you have a `defineAuth` config with `admin` and `auditor` groups. |
| 104 | + |
| 105 | +```ts title="amplify/auth/resource.ts" |
| 106 | +import { defineAuth } from '@aws-amplify/backend'; |
| 107 | + |
| 108 | +export const auth = defineAuth({ |
| 109 | + loginWith: { |
| 110 | + email: true |
| 111 | + }, |
| 112 | + groups: ['auditor', 'admin'] |
| 113 | +}); |
| 114 | +``` |
| 115 | + |
| 116 | +With the following `access` definition, you can configure permissions such that auditors have readonly permissions to `foo/*` while admin has full permissions. |
| 117 | + |
| 118 | +```ts title="amplify/storage/resource.ts" |
| 119 | +export const storage = defineStorage({ |
| 120 | + name: 'myProjectFiles', |
| 121 | + access: (allow) => ({ |
| 122 | + 'foo/*': [ |
| 123 | + allow.group('auditor').to(['read']), |
| 124 | + allow.group('admin').to(['read', 'write', 'delete']) |
| 125 | + ] |
| 126 | + }) |
| 127 | +}); |
| 128 | +``` |
| 129 | + |
| 130 | +### Owner-based access |
| 131 | + |
| 132 | +Access to files with a certain prefix can be scoped down to individual authenticated users. To do this, a placeholder token is used in the storage path which will be substituted with the user identity when uploading or downloading files. The access rule will only allow a user to upload or download files with their specific identity string. |
| 133 | + |
| 134 | +Note that your backend must include `defineAuth` in order to use this access rule. |
| 135 | + |
| 136 | +The following policy would allow authenticated users full access to files with a prefix that matches their identity id. |
| 137 | + |
| 138 | +```ts title="amplify/storage/resource.ts" |
| 139 | +export const storage = defineStorage({ |
| 140 | + name: 'myProjectFiles', |
| 141 | + access: (allow) => ({ |
| 142 | + 'foo/{entity_id}/*': [ |
| 143 | + // {entity_id} is the token that is replaced with the user identity id |
| 144 | + allow.entity('identity').to(['read', 'write', 'delete']) |
| 145 | + ] |
| 146 | + }) |
| 147 | +}); |
| 148 | +``` |
| 149 | + |
| 150 | +A user with identity id "123" would be able to perform read/write/delete operations on files within `foo/123/*` and would not be able to perform actions on files with any other prefix. Likewise, a user with identity id "ABC" would be able to perform read/write/delete operation on files only within `foo/ABC/*`. In this way, each user can be granted access to a "private storage location" that is not accessible to any other user. |
| 151 | + |
| 152 | +It may be desireable for a file owner to be able to write and delete files in their private location but allow anyone to read from that location. For example, profile pictures should be readable by anyone, but only the owner can modify them. This use case can be configured with the following definition. |
| 153 | + |
| 154 | +```ts title="amplify/storage/resource.ts" |
| 155 | +export const storage = defineStorage({ |
| 156 | + name: 'myProjectFiles', |
| 157 | + access: (allow) => ({ |
| 158 | + 'foo/{entity_id}/*': [ |
| 159 | + allow.entity('identity').to(['read', 'write', 'delete']), |
| 160 | + allow.guest.to(['read']), |
| 161 | + allow.authenticated.to(['read']) |
| 162 | + ] |
| 163 | + }) |
| 164 | +}); |
| 165 | +``` |
| 166 | + |
| 167 | +When a non-id-based rule is applied to a path with the `{entity_id}` token, the token is replaced with a wildcard (`*`). This means that the access will apply to files uploaded by _any_ user. In the above policy, write and delete is scoped to just the owner, but read is allowed for guest and authenticated users for any file within `foo/*/*`. |
| 168 | + |
| 169 | +### Grant function access |
| 170 | + |
| 171 | +In addition to granting application users access to storage files, you may also want to grant a backend function access to storage files. This could be used to enable a use case like resizing images, or automatically deleting old files. The following configuration is used to define function access. |
| 172 | + |
| 173 | +```ts title="amplify/storage/resource.ts" |
| 174 | +import { defineStorage, defineFunction } from '@aws-amplify/backend'; |
| 175 | + |
| 176 | +const demoFunction = defineFunction({}); |
| 177 | + |
| 178 | +export const storage = defineStorage({ |
| 179 | + name: 'myProjectFiles', |
| 180 | + access: (allow) => ({ |
| 181 | + 'foo/*': [allow.resource(demoFunction).to(['read', 'write', 'delete'])] |
| 182 | + }) |
| 183 | +}); |
| 184 | +``` |
| 185 | + |
| 186 | +This would grant the function `demoFunction` the ability to read write and delete files within `foo/*`. |
| 187 | + |
| 188 | +When a function is granted access to storage, it also receives an environment variable that contains the name of the S3 bucket configured by storage. This environment variable can be used in the function to make SDK calls to the storage bucket. The environment variable is named `<storageName>_BUCKET_NAME`. In the above example, it would be named `myProjectFiles_BUCKET_NAME`. |
| 189 | + |
| 190 | +[Learn more about function resource access environment variables](/gen2/build-a-backend/functions/#resource-access) |
| 191 | + |
| 192 | +### Access definition limitations |
| 193 | + |
| 194 | +There are some limitations on the types of prefixes that can be specified in the storage access definition. |
| 195 | + |
| 196 | +1. All paths start at the storage root. Paths cannot be defined relative to other paths. |
| 197 | +2. All paths are treated as prefixes. To make this explicit, all paths must end with `/*`. |
| 198 | +3. Only one level of nesting is allowed. For example, you can define access controls on `foo/*` and `foo/bar/*` but not on `foo/bar/baz/*` because that path has 2 other prefixes. |
| 199 | +4. Wildcards cannot conflict with the `{entity_id}` token. For example, you cannot have both `foo/*` and `foo/{entity_id}/*` defined because the wildcard in the first path conflicts with the `{entity_id}` token in the second path. |
| 200 | +5. A path cannot be a prefix of another path with an `{entity_id}` token. For example `foo/*` and `foo/bar/{entity_id}/*` is not allowed. |
| 201 | + |
| 202 | +### Prefix behavior |
| 203 | + |
| 204 | +When one path is a subpath of another, the permissions on the subpath _always override_ the permissions from the parent path. Permissions are not "inherited" from a parent path. Consider the following access definition example. |
| 205 | + |
| 206 | +```ts |
| 207 | +export const storage = defineStorage({ |
| 208 | + name: 'myProjectFiles', |
| 209 | + access: (allow) => ({ |
| 210 | + 'foo/*': [allow.authenticated.to(['read', 'write', 'delete'])], |
| 211 | + 'foo/bar/*': [allow.guest.to(['read'])], |
| 212 | + 'foo/baz/*': [allow.authenticated.to(['read'])], |
| 213 | + 'other/*': [ |
| 214 | + allow.guest.to(['read']), |
| 215 | + allow.authenticated.to(['read', 'write']) |
| 216 | + ] |
| 217 | + }) |
| 218 | +}); |
| 219 | +``` |
| 220 | + |
| 221 | +The access control matrix for this configuration is |
| 222 | + |
| 223 | +| | foo/\* | foo/bar/\* | foo/baz/\* | other/\* | |
| 224 | +| --- | --- | --- | --- | --- | |
| 225 | +| **Authenticated Users** | read, write, delete | NONE | read | read, write | |
| 226 | +| **Guest users** | NONE | read | NONE | read | |
| 227 | + |
| 228 | +Authenticated users have access to read, write, and delete everything under `foo/*` EXCEPT `foo/bar/*` and `foo/baz/*`. For those subpaths, the scoped down access overrides the access granted on the parent `foo/*` |
| 229 | + |
| 230 | +### Available actions |
| 231 | + |
| 232 | +When you configure access to a particular storage prefix, you can scope the access to one or more CRUDL actions. |
| 233 | + |
| 234 | +#### `read` |
| 235 | + |
| 236 | +This is a convenience action that is equivalent to setting both `get` and `list` access. |
| 237 | + |
| 238 | +#### `get` |
| 239 | + |
| 240 | +This action maps to the [`s3:GetObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) IAM action, scoped to the corresponding object prefix. |
| 241 | + |
| 242 | +#### `list` |
| 243 | + |
| 244 | +This action maps to the [`s3:ListBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) IAM action, scoped to the corresponding object prefix. |
| 245 | + |
| 246 | +#### `write` |
| 247 | + |
| 248 | +This action maps to the [`s3:PutObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) IAM action, scoped to the corresponding object prefix. Note that this action grants the ability to both create new object and update existing ones. There is no way to scope access to only creating or only updating objects. |
| 249 | + |
| 250 | +#### `delete` |
| 251 | + |
| 252 | +This action maps to the [`s3:DeleteObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html) IAM action, scoped to the corresponding object prefix. |
| 253 | + |
| 254 | +### Configuring Amplify Gen 1-equivalent access patterns |
| 255 | + |
| 256 | +To configure `defineStorage` in Amplify Gen 2 to behave the same way as the storage category in Gen 1, the following definition can be used. |
| 257 | + |
| 258 | +```ts title="amplify/storage/resource.ts" |
| 259 | +export const storage = defineStorage({ |
| 260 | + name: 'myProjectFiles', |
| 261 | + access: (allow) => ({ |
| 262 | + 'public/*': [ |
| 263 | + allow.guest.to(['read']), |
| 264 | + allow.authenticated.to(['read', 'write', 'delete']), |
| 265 | + ], |
| 266 | + 'protected/{entity_id}/*': [ |
| 267 | + allow.authenticated.to(['read']), |
| 268 | + allow.entity('identity').to(['read', 'write', 'delete']) |
| 269 | + ], |
| 270 | + 'private/{entity_id}/*': [allow.entity('identity').to(['read', 'write', 'delete'])] |
| 271 | + }) |
| 272 | +}); |
| 273 | +``` |
| 274 | + |
| 275 | +## Configure storage triggers |
| 276 | + |
| 277 | +Function triggers can be configured to enable event-based workflows when files are uploaded or deleted. To add a function trigger, modify the `defineStorage` configuration. |
| 278 | + |
| 279 | +First, in your storage definition, add the following: |
| 280 | + |
| 281 | +```ts title="amplify/storage/resource.ts" |
| 282 | +export const storage = defineStorage({ |
| 283 | + name: 'myProjectFiles', |
| 284 | + // highlight-start |
| 285 | + triggers: { |
| 286 | + onUpload: defineFunction({ |
| 287 | + entry: './on-upload-handler.ts' |
| 288 | + }), |
| 289 | + onDelete: defineFunction({ |
| 290 | + entry: './on-delete-handler.ts' |
| 291 | + }) |
| 292 | + } |
| 293 | + // highlight-end |
| 294 | +}); |
| 295 | +``` |
| 296 | + |
| 297 | +Then create the function definitions at `amplify/storage/on-upload-handler.ts` and `amplify/storage/on-delete-handler.ts`. |
| 298 | + |
| 299 | +```ts title="amplify/storage/on-upload-handler.ts" |
| 300 | +import type { S3Handler } from 'aws-lambda'; |
| 301 | + |
| 302 | +export const handler: S3Handler = async (event) => { |
| 303 | + const objectKeys = event.Records.map((record) => record.s3.object.key); |
| 304 | + console.log(`Upload handler invoked for objects [${objectKeys.join(', ')}]`); |
| 305 | +}; |
| 306 | +``` |
| 307 | + |
| 308 | +```ts title="amplify/storage/on-delete-handler.ts" |
| 309 | +import type { S3Handler } from 'aws-lambda'; |
| 310 | + |
| 311 | +export const handler: S3Handler = async (event) => { |
| 312 | + const objectKeys = event.Records.map((record) => record.s3.object.key); |
| 313 | + console.log(`Delete handler invoked for objects [${objectKeys.join(', ')}]`); |
| 314 | +}; |
| 315 | +``` |
| 316 | + |
| 317 | +<Callout info> |
| 318 | + |
| 319 | +**Note:** The `S3Handler` type comes from the [@types/aws-lambda](https://www.npmjs.com/package/@types/aws-lambda) npm package. This package contains types for different kinds of Lambda handlers, events, and responses. |
| 320 | + |
| 321 | +</Callout> |
| 322 | + |
| 323 | +Now, when you deploy your backend, these functions will be invoked whenever an object is uploaded or deleted from the bucket. |
| 324 | + |
| 325 | +### Next steps |
| 326 | + |
| 327 | +- [Learn more about `s3.Bucket`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html) |
| 328 | +- [Learn more about `defineBackend`](/gen2/build-a-backend/) |
0 commit comments