File access levels
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 (you need to specify the identityID of the user who uploaded the file). Writable only 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.
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:
func uploadData(key: String, data: Data) async throws { let options = StorageUploadDataRequest.Options(accessLevel: .protected) let uploadTask = Amplify.Storage.uploadData( key: key, data: data, options: options ) Task { for await progress in await uploadTask.progress { print("Progress: \(progress)") } } let data = try await uploadTask.value print("Completed: \(data)")}
var resultSink: AnyCancellable?var progressSink: AnyCancellable?
func uploadData(key: String, data: Data) { let options = StorageUploadDataRequest.Options(accessLevel: .protected) let uploadTask = Amplify.Storage.uploadData( key: key, data: data, options: options ) progressSink = uploadTask .inProcessPublisher .sink { progress in print("Progress: \(progress)") }
resultSink = uploadTask .resultPublisher .sink { if case let .failure(storageError) = $0 { print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)") } } receiveValue: { data in print("Completed: \(data)") }}
This will upload with the prefix /protected/[IDENTITY_ID]/
followed by the key
.
For other users to read the file, you must specify the access level as protected
and the identity ID of the user who uploaded it in the options.
func downloadData(key: String, identityId: String) async throws { let options = StorageDownloadDataRequest.Options( accessLevel: .protected, targetIdentityId: identityId )
let downloadTask = Amplify.Storage.downloadData( key: key, options: options )
Task { for await progress in await downloadTask.progress { print("Progress: \(progress)") } } let data = try await downloadTask.value print("Completed: \(data)")}
var progressSink: AnyCancellable?var resultSink: AnyCancellable?
func downloadData(key: String, identityId: String) { let options = StorageDownloadDataRequest.Options( accessLevel: .protected, targetIdentityId: identityId ) let downloadTask = Amplify.Storage.downloadData( key: key, options: options ) progressSink = downloadTask .inProcessPublisher .sink { progress in print("Progress: \(progress)") }
resultSink = downloadTask .resultPublisher .sink { if case let .failure(storageError) = $0 { print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)") } } receiveValue: { data in print("Completed: \(data)") }}
Private Access
Create an options object specifying the private access level to only allow an object to be accessed by the uploading user
let options = StorageUploadDataRequest.Options(accessLevel: .private)
This will upload with the prefix /private/[IDENTITY_ID]/
, followed by the key
.
For the uploading user to read the file, specify the same access level (private
) and key you used to upload:
let options = StorageDownloadDataRequest.Options(accessLevel: .private)
Customization (Deprecated)
Customize Object Key Path
You can customize your key path by defining a prefix resolver:
import Amplifyimport AWSPluginsCoreimport AWSS3StoragePlugin
// Define your own prefix resolver, that conforms to `AWSS3StoragePluginPrefixResolver`struct MyDeveloperDefinedPrefixResolver: AWSS3PluginPrefixResolver {
// This function is called on every Storage API to modify the prefix of the request. func resolvePrefix( for accessLevel: StorageAccessLevel, targetIdentityId: String? ) async throws -> String { // Use "myPublicPrefix" for guest access levels if accessLevel == .guest { return "myPublicPrefix/" }
// Use "myProtectedPrefix/{identityId}" and "myPrivatePrefix/{identityId}" respectively let accessLevelPrefix: String if accessLevel == .protected { accessLevelPrefix = "myProtectedPrefix/" } else { accessLevelPrefix = "myPrivatePrefix/" }
// `targetIdentityId` is the value passed into the Storage request object if let identityId = targetIdentityId { return accessLevelPrefix + identityId + "/" }
// `identityId` is the identity id of the current user let identityId = try await getIdentityId() return accessLevelPrefix + identityId + "/" }
func getIdentityId() async throws -> String { let session = try await Amplify.Auth.fetchAuthSession() if let identityProvider = session as? AuthCognitoIdentityProvider { return try identityProvider.getIdentityId().get() } else { throw StorageError.authError("Unable to retrieve identity id", "", nil) } }}
Then configure the storage plugin with the resolver.
let storagePlugin = AWSS3StoragePlugin(configuration: .prefixResolver(MyDeveloperDefinedPrefixResolver()))Amplify.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/:
"Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject" ], "Resource": [ "arn:aws:s3:::your-s3-bucket/myPublicPrefix/*", ] }]
If you want to have custom private path prefix like myPrivatePrefix/, you need to add it into your IAM policy:
"Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject" ], "Resource": [ "arn:aws:s3:::your-s3-bucket/myPrivatePrefix/${cognito-identity.amazonaws.com:sub}/*" ] }]
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:
func resolvePrefix( for accessLevel: StorageAccessLevel, targetIdentityId: String?) async throws -> String { return ""}
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:
struct MyDeveloperDefinedPrefixResolver: AWSS3PluginPrefixResolver { func resolvePrefix( for accessLevel: StorageAccessLevel, targetIdentityId: String? ) async throws -> String { guard await Amplify.Auth.getCurrentUser() != nil else { throw StorageError.authError("User is not signed in", "", nil) }
// Continue to resolve the prefix for the request // ... }}