Page updated Jan 16, 2024

File access levels

Amplify iOS v1 is now in Maintenance Mode until May 31st, 2024. This means that we will continue to include updates to ensure compatibility with backend services and security. No new features will be introduced in v1.

Please use the latest version (v2) of Amplify Library for Swift to get started.

If you are currently using v1, follow these instructions to upgrade to v2.

Amplify libraries should be used for all new cloud connected applications. If you are currently using the AWS Mobile SDK for iOS, you can access the documentation here.

When adding the Storage category, you configure the level of access users have to your S3 bucket. You can configure separate rules for authenticated vs. guest users. When using the Storage category to upload files, you can also specify an access level for each individual file: guest, protected, or private.

  • Guest Accessible by all users of your application
  • Protected Readable by all users, but only writable by the creating user
  • Private Readable and writable only by the creating user

Guest access does not mean that your files are totally public. A "guest" is a user of your application who has not yet signed in. To enable access at this level, you will still be required to configured Authentication in your app. The user must be able to assume an unauthenticated role from your Cognito Identity Pool.

For protected and private access, the [IDENTITY_ID] below corresponds to the unique ID of the user. Once the user has signed in, the [IDENTITY_ID] can be retrieved from the session by accessing the identity id. See Accessing credentials to retrieve the identity id, and use this as the unique ID of the authenticated user.

The default access level for the Storage category is guest. Unless you specify otherwise, all uploaded files will be available to all users of your application. This means that a user who is using your application but has not signed in will have access. Anyone else who is not using your application will not be able to access your files.

Protected access

After the user has signed in, create an options object specifying the protected access level to allow other users to read the object:

1func uploadData(key: String, data: Data) {
2 let options = StorageUploadDataRequest.Options(accessLevel: .protected)
3 Amplify.Storage.uploadData(key: key, data: data, options: options) { progress in
4 print("Progress: \(progress)")
5 } resultListener: { event in
6 switch event {
7 case .success(let data):
8 print("Completed: \(data)")
9 case .failure(let storageError):
10 print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
11 }
12 }
13}
1var progressSink: AnyCancellable?
2var resultSink: AnyCancellable?
3
4func uploadData(key: String, data: Data) {
5 let options = StorageUploadDataRequest.Options(accessLevel: .protected)
6 let storageOperation = Amplify.Storage.uploadData(key: key, data: data, options: options)
7 progressSink = storageOperation.progressPublisher.sink { progress in print("Progress: \(progress)") }
8 resultSink = storageOperation.resultPublisher.sink {
9 if case let .failure(storageError) = $0 {
10 print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
11 }
12 }
13 receiveValue: { data in
14 print("Completed: \(data)")
15 }
16}

This will upload with the prefix /protected/[IDENTITY_ID]/ followed by the key.

For other users to read the file, you must specify the user ID of the creating user in the passed options.

1func downloadData(key: String, identityId: String) {
2 let options = StorageDownloadDataRequest.Options(accessLevel: .protected, targetIdentityId: identityId)
3 Amplify.Storage.downloadData(
4 key: key,
5 options: options,
6 progressListener: { progress in
7 print("Progress: \(progress)")
8 }, resultListener: { (event) in
9 switch event {
10 case let .success(data):
11 print("Completed: \(data)")
12 case let .failure(storageError):
13 print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
14 }
15 })
16}
1var progressSink: AnyCancellable?
2var resultSink: AnyCancellable?
3
4func downloadData(key: String, identityId: String) {
5 let options = StorageDownloadDataRequest.Options(accessLevel: .protected,
6 targetIdentityId: identityId)
7 let storageOperation = Amplify.Storage.downloadData(key: key, options: options)
8 progressSink = storageOperation.progressPublisher.sink { progress in print("Progress: \(progress)") }
9 resultSink = storageOperation.resultPublisher.sink {
10 if case let .failure(storageError) = $0 {
11 print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
12 }
13 }
14 receiveValue: { data in
15 print("Completed: \(data)")
16 }
17}

Private Access

Create an options object specifying the private access level to only allow an object to be accessed by the creating user

1let options = StorageUploadDataRequest.Options(accessLevel: .private)

This will upload with the prefix /private/[IDENTITY_ID]/, followed by the key.

For the user to read the file, specify the same access level (private) and key you used to upload:

1let options = StorageDownloadDataRequest.Options(accessLevel: .private)

Customization

Customize Object Key Path

You can customize your key path by defining a prefix resolver:

1import Amplify
2import AmplifyPlugins
3
4// Define your own prefix resolver, that conforms to `AWSS3StoragePluginPrefixResolver`
5struct MyDeveloperDefinedPrefixResolver: AWSS3PluginPrefixResolver {
6
7 // This function is called on every Storage API to modify the prefix of the request.
8 func resolvePrefix(
9 for accessLevel: StorageAccessLevel,
10 targetIdentityId: String?,
11 completion: @escaping (Result<String, StorageError>) -> Void
12 ) {
13 // Use "myPublicPrefix" for guest access levels
14 if accessLevel == .guest {
15 completion(.success("myPublicPrefix/"))
16 return
17 }
18
19 // Use "myProtectedPrefix/{targetIdentityId}" and "myPrivatePrefix/{targetIdentityId}" respectively
20 // `targetIdentityId` is the value passed into the Storage request object,
21 if let identityId = targetIdentityId {
22 if accessLevel == .protected {
23 completion(.success("myProtectedPrefix/" + identityId + "/"))
24 } else if accessLevel == .private {
25 completion(.success("myPrivatePrefix/" + identityId + "/"))
26 }
27 return
28 }
29
30 // Use "myProtectedPrefix/{identityId}" and "myPrivatePrefix/{identityId}" respectively
31 // `identityId` is the identity id of the current user
32 getIdentityId { result in
33 switch result {
34 case .failure(let error):
35 completion(.failure(error))
36 case .success(let identityId):
37 if accessLevel == .protected {
38 completion(.success("myProtectedPrefix/" + identityId + "/"))
39 } else if accessLevel == .private {
40 completion(.success("myPrivatePrefix/" + identityId + "/"))
41 }
42 }
43 }
44 }
45
46 public func getIdentityId(completion: @escaping (Result<String, StorageError>) -> Void) {
47 Amplify.Auth.fetchAuthSession { result in
48 do {
49 let session = try result.get()
50 if let identityProvider = session as? AuthCognitoIdentityProvider {
51 let identityId = try identityProvider.getIdentityId().get()
52 completion(.success(identityId))
53 }
54 } catch {
55 completion(.failure(StorageError.authError("Failed to retrieve auth session", "", error)))
56 }
57 }
58 }
59}

Then configure the storage plugin with the resolver.

1let storagePlugin = AWSS3StoragePlugin(configuration: .prefixResolver(MyDeveloperDefinedPrefixResolver()))
2Amplify.add(storagePlugin)

Add the IAM policy that corresponds with the prefixes defined above to enable read, write and delete operation for all the objects under path myPublicPrefix/:

1"Statement": [
2 {
3 "Effect": "Allow",
4 "Action": [
5 "s3:GetObject",
6 "s3:PutObject",
7 "s3:DeleteObject"
8 ],
9 "Resource": [
10 "arn:aws:s3:::your-s3-bucket/myPublicPrefix/*",
11 ]
12 }
13]

If you want to have custom private path prefix like myPrivatePrefix/, you need to add it into your IAM policy:

1"Statement": [
2 {
3 "Effect": "Allow",
4 "Action": [
5 "s3:GetObject",
6 "s3:PutObject",
7 "s3:DeleteObject"
8 ],
9 "Resource": [
10 "arn:aws:s3:::your-s3-bucket/myPrivatePrefix/${cognito-identity.amazonaws.com:sub}/*"
11 ]
12 }
13]

This ensures only the authenticated users has the access to the objects under the path.

Passthrough PrefixResolver

If you would like no prefix resolution logic, such as performing S3 requests at the root of the bucket, create a prefix resolver that returns an empty string:

1public func resolvePrefix(
2 for accessLevel: StorageAccessLevel,
3 targetIdentityId: String?
4) -> Result<String, StorageError> {
5 return .success("")
6}

Client validation

You can also perform validation based on the access controls you have defined. For example, if you have defined Guests with no access then you can fail the request early by checking if the user is not signed in:

This will only stop your app from making a call with an unauthenticated user. You must also set up your IAM policies to protect your resources. See CLI Storage for more details.

1struct MyDeveloperDefinedPrefixResolver: AWSS3PluginPrefixResolver {
2 public func resolvePrefix(
3 for accessLevel: StorageAccessLevel,
4 targetIdentityId: String?,
5 completion: @escaping (Result<String, StorageError>) -> Void
6 ) {
7 guard Amplify.Auth.getCurrentUser() != nil else {
8 completion(.failure(.authError("User is not signed in", "", nil)))
9 return
10 }
11
12 // Continue to resolve the prefix for the request
13 // ...
14 }
15}