From baa9331ad0acb02abe48bb1d054871913fb0e290 Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Wed, 16 Oct 2024 15:43:28 -0400 Subject: [PATCH] Swift: Add a static credential identity resolver example (#6979) --- .../example_code/sts/AssumeRole/Package.swift | 40 +++ .../sts/AssumeRole/Sources/entry.swift | 277 ++++++++++++++++++ swift/example_code/sts/README.md | 89 ++++++ 3 files changed, 406 insertions(+) create mode 100644 swift/example_code/sts/AssumeRole/Package.swift create mode 100644 swift/example_code/sts/AssumeRole/Sources/entry.swift create mode 100644 swift/example_code/sts/README.md diff --git a/swift/example_code/sts/AssumeRole/Package.swift b/swift/example_code/sts/AssumeRole/Package.swift new file mode 100644 index 00000000000..2669cd7015f --- /dev/null +++ b/swift/example_code/sts/AssumeRole/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version:5.9 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// The swift-tools-version declares the minimum version of Swift required to +// build this package. + +import PackageDescription + +let package = Package( + name: "AssumeRole", + // Let Xcode know the minimum Apple platforms supported. + platforms: [ + .macOS(.v11), + .iOS(.v13) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package( + url: "https://github.com/awslabs/aws-sdk-swift", + from: "1.0.0" + ), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + branch: "main" + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "AssumeRole", + dependencies: [ + .product(name: "AWSSTS", package: "aws-sdk-swift"), + .product(name: "AWSS3", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Sources"), + ] +) diff --git a/swift/example_code/sts/AssumeRole/Sources/entry.swift b/swift/example_code/sts/AssumeRole/Sources/entry.swift new file mode 100644 index 00000000000..9f8dabb1d39 --- /dev/null +++ b/swift/example_code/sts/AssumeRole/Sources/entry.swift @@ -0,0 +1,277 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +/// A simple example that shows how to use the AWS SDK for Swift to +/// authenticate using optional static credentials and an AWS IAM role ARN. + +// snippet-start:[swift.sts.AssumeRole.imports] +import ArgumentParser +import AWSClientRuntime +import AWSS3 +import AWSSDKIdentity +import AWSSTS +import Foundation +import SmithyIdentity +// snippet-end:[swift.sts.AssumeRole.imports] + +struct ExampleCommand: ParsableCommand { + @Option(help: "AWS access key ID") + var accessKey: String? = nil + @Option(help: "AWS secret access key") + var secretKey: String? = nil + @Option(help: "Session token") + var sessionToken: String? = nil + @Argument(help: "ARN of the role to assume") + var roleArn: String + + static var configuration = CommandConfiguration( + commandName: "AssumeRole", + abstract: """ + Authenticate using the specified role, optionally using specified + access key, secret access key, and session token first. + """, + discussion: """ + This program uses the specified access key, secret access key, and + optional session token, to request temporary credentials for the + specified role Then it uses the credentials to list the user's + Amazon S3 buckets. This shows a couple of ways to use a + StaticAWSCredentialIdentityResolver object. + """ + ) + + /// Called by ``main()`` to do the actual running of the AWS + /// example. + // snippet-start:[swift.sts.AssumeRole.command.runasync] + func runAsync() async throws { + // If credentials are specified, create a credential identity + // resolver that uses them to authenticate. This identity will be used + // to ask for permission to use the specified role. + + var identityResolver: StaticAWSCredentialIdentityResolver? = nil + + if accessKey != nil && secretKey != nil { + do { + identityResolver = try getIdentityResolver(accessKey: accessKey, + secretKey: secretKey, sessionToken: sessionToken) + } catch { + print("ERROR: Unable to get identity resolver in runAsync:", + dump(error)) + throw error + } + } + + // Assume the role using the credentials provided on the command line, + // or using the default credentials if none were specified. + + do { + // snippet-start: [swift.sts.AssumeRole] + let credentials = try await assumeRole(identityResolver: identityResolver, + roleArn: roleArn) + do { + identityResolver = try getIdentityResolver( + accessKey: credentials.accessKey, + secretKey: credentials.secret, + sessionToken: credentials.sessionToken + ) + } catch { + print("ERROR: Unable to authenticate using provided options:", + dump(error)) + throw error + } + // snippet-end: [swift.sts.AssumeRole] + } catch { + print("ERROR: Error assuming role in runAsync:", dump(error)) + throw AssumeRoleExampleError.assumeRoleFailed + } + + // Use the credential identity resolver to access AWS S3. + + do { + let names = try await getBucketNames(identityResolver: identityResolver) + + print("Found \(names.count) buckets:") + for name in names { + print(" \(name)") + } + } catch { + print("ERROR: Error getting bucket names in runAsync:", dump(error)) + throw error + } + } + // snippet-end:[swift.sts.AssumeRole.command.runasync] +} + +/// An `Error` type used to return errors from the +/// `assumeRole(identityResolver: roleArn:)` function. +enum AssumeRoleExampleError: Error { + /// An error indicating that the STS `AssumeRole` request failed. + case assumeRoleFailed + /// An error indicating that the returned credentials were missing + /// required information. + case incompleteCredentials + /// An error indicating that no credentials were returned by `AssumeRole`. + case missingCredentials + + /// Return a human-readable explanation of the error. + var errorDescription: String? { + switch self { + case .assumeRoleFailed: + return "Unable to assume the specified role." + case .incompleteCredentials: + return "AWS STS returned incomplete credentials." + case .missingCredentials: + return "AWS STS did not return any credentials for the specified role." + } + } +} + +// snippet-start:[swift.sts.AssumeRole.assumeRole-function] +/// Assume the specified role. If any kind of credential identity resolver is +/// specified, that identity is adopted before assuming the role. +/// +/// - Parameters: +/// - identityResolver: Any kind of `AWSCredentialIdentityResolver`. If +/// provided, this identity is adopted before attempting to assume the +/// specified role. +/// - roleArn: The ARN of the AWS role to assume. +/// +/// - Throws: Re-throws STS errors. Also can throw any +/// `AssumeRoleExampleError`. +/// - Returns: An `AWSCredentialIdentity` containing the temporary credentials +/// assigned. +func assumeRole(identityResolver: (any AWSCredentialIdentityResolver)?, + roleArn: String) async throws -> AWSCredentialIdentity { + let stsConfiguration = try await STSClient.STSClientConfiguration( + awsCredentialIdentityResolver: identityResolver + ) + let stsClient = STSClient(config: stsConfiguration) + + // Assume the role and return the assigned credentials. + + // snippet-start: [swift.sts.sts.AssumeRole] + let input = AssumeRoleInput( + roleArn: roleArn, + roleSessionName: "AssumeRole-Example" + ) + + let output = try await stsClient.assumeRole(input: input) + + guard let credentials = output.credentials else { + throw AssumeRoleExampleError.missingCredentials + } + + guard let accessKey = credentials.accessKeyId, + let secretKey = credentials.secretAccessKey, + let sessionToken = credentials.sessionToken else { + throw AssumeRoleExampleError.incompleteCredentials + } + // snippet-end: [swift.sts.sts.AssumeRole] + + // Return an `AWSCredentialIdentity` object with the temporary + // credentials. + + let awsCredentials = AWSCredentialIdentity( + accessKey: accessKey, + secret: secretKey, + sessionToken: sessionToken + ) + return awsCredentials +} +// snippet-end:[swift.sts.AssumeRole.assumeRole-function] + +/// Return an array containing the names of all available buckets using +/// the specified credential identity resolver to authenticate. +/// +/// - Parameter identityResolver: Any type of `AWSCredentialIdentityResolver`, +/// used to authenticate and authorize the user for access to the bucket +/// names. +/// +/// - Throws: Re-throws errors from `ListBucketsPaginated`. +/// +/// - Returns: An array of strings listing the buckets. +func getBucketNames(identityResolver: (any AWSCredentialIdentityResolver)?) + async throws -> [String] { + do { + // Get an S3Client with which to access Amazon S3. + // snippet-start:[swift.sts.AssumeRole.use-resolver] + let configuration = try await S3Client.S3ClientConfiguration( + awsCredentialIdentityResolver: identityResolver + ) + let client = S3Client(config: configuration) + + // Use "Paginated" to get all the buckets. This lets the SDK handle + // the 'continuationToken' in "ListBucketsOutput". + let pages = client.listBucketsPaginated( + input: ListBucketsInput( maxBuckets: 10) + ) + // snippet-end:[swift.sts.AssumeRole.use-resolver] + + // Get the bucket names. + var bucketNames: [String] = [] + + do { + for try await page in pages { + guard let buckets = page.buckets else { + print("Error: page is empty.") + continue + } + + for bucket in buckets { + bucketNames.append(bucket.name ?? "") + } + } + + return bucketNames + } catch { + print("ERROR: listBuckets:", dump(error)) + throw error + } + } +} + +/// Create a credential identity resolver using access key and secret access +/// key. +/// +/// - Parameters: +/// - accessKey: A string containing the AWS access key ID. +/// - secretKey: A string containing the AWS secret access key. +/// - sessionToken: An optional string containing the session token. +/// - Throws: Re-throws errors from AWSSDKIdentity. +/// - Returns: A `StaticAWSCredentialIdentityResolver` that can be used when +/// configuring service clients. +func getIdentityResolver(accessKey: String?, secretKey: String?, + sessionToken: String?) + throws -> StaticAWSCredentialIdentityResolver? { + + if accessKey == nil || secretKey == nil { + return nil + } + + guard let accessKey = accessKey, + let secretKey = secretKey else { + return nil + } + + let credentials = AWSCredentialIdentity( + accessKey: accessKey, + secret: secretKey, + sessionToken: sessionToken + ) + + return try StaticAWSCredentialIdentityResolver(credentials) +} + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/sts/README.md b/swift/example_code/sts/README.md new file mode 100644 index 00000000000..0547ddec2ff --- /dev/null +++ b/swift/example_code/sts/README.md @@ -0,0 +1,89 @@ +# AWS STS code examples for the SDK for Swift + +## Overview + +Shows how to use the AWS SDK for Swift to work with AWS Security Token Service (AWS STS). + + + + +_AWS STS creates and provides trusted users with temporary security credentials that can control access to your AWS resources._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `swift` folder. + + + + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [AssumeRole](../iam/basics/Sources/ServiceHandler/ServiceHandlerSTS.swift#L160) + + + + + +## Run the examples + +### Instructions + +To build any of these examples from a terminal window, navigate into its +directory, then use the following command: + +``` +$ swift build +``` + +To build one of these examples in Xcode, navigate to the example's directory +(such as the `ListUsers` directory, to build that example). Then type `xed.` +to open the example directory in Xcode. You can then use standard Xcode build +and run commands. + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../../README.md#Tests) +in the `swift` folder. + + + + + + +## Additional resources + +- [AWS STS User Guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) +- [AWS STS API Reference](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) +- [SDK for Swift AWS STS reference](https://sdk.amazonaws.com/swift/api/awssts/latest/documentation/awssts) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file