Skip to content

Commit baa9331

Browse files
authored
Swift: Add a static credential identity resolver example (#6979)
1 parent 25605a6 commit baa9331

File tree

3 files changed

+406
-0
lines changed

3 files changed

+406
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// swift-tools-version:5.9
2+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// The swift-tools-version declares the minimum version of Swift required to
6+
// build this package.
7+
8+
import PackageDescription
9+
10+
let package = Package(
11+
name: "AssumeRole",
12+
// Let Xcode know the minimum Apple platforms supported.
13+
platforms: [
14+
.macOS(.v11),
15+
.iOS(.v13)
16+
],
17+
dependencies: [
18+
// Dependencies declare other packages that this package depends on.
19+
.package(
20+
url: "https://github.com/awslabs/aws-sdk-swift",
21+
from: "1.0.0"
22+
),
23+
.package(
24+
url: "https://github.com/apple/swift-argument-parser.git",
25+
branch: "main"
26+
),
27+
],
28+
targets: [
29+
// Targets are the basic building blocks of a package, defining a module or a test suite.
30+
// Targets can depend on other targets in this package and products from dependencies.
31+
.executableTarget(
32+
name: "AssumeRole",
33+
dependencies: [
34+
.product(name: "AWSSTS", package: "aws-sdk-swift"),
35+
.product(name: "AWSS3", package: "aws-sdk-swift"),
36+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
37+
],
38+
path: "Sources"),
39+
]
40+
)
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
/// A simple example that shows how to use the AWS SDK for Swift to
5+
/// authenticate using optional static credentials and an AWS IAM role ARN.
6+
7+
// snippet-start:[swift.sts.AssumeRole.imports]
8+
import ArgumentParser
9+
import AWSClientRuntime
10+
import AWSS3
11+
import AWSSDKIdentity
12+
import AWSSTS
13+
import Foundation
14+
import SmithyIdentity
15+
// snippet-end:[swift.sts.AssumeRole.imports]
16+
17+
struct ExampleCommand: ParsableCommand {
18+
@Option(help: "AWS access key ID")
19+
var accessKey: String? = nil
20+
@Option(help: "AWS secret access key")
21+
var secretKey: String? = nil
22+
@Option(help: "Session token")
23+
var sessionToken: String? = nil
24+
@Argument(help: "ARN of the role to assume")
25+
var roleArn: String
26+
27+
static var configuration = CommandConfiguration(
28+
commandName: "AssumeRole",
29+
abstract: """
30+
Authenticate using the specified role, optionally using specified
31+
access key, secret access key, and session token first.
32+
""",
33+
discussion: """
34+
This program uses the specified access key, secret access key, and
35+
optional session token, to request temporary credentials for the
36+
specified role Then it uses the credentials to list the user's
37+
Amazon S3 buckets. This shows a couple of ways to use a
38+
StaticAWSCredentialIdentityResolver object.
39+
"""
40+
)
41+
42+
/// Called by ``main()`` to do the actual running of the AWS
43+
/// example.
44+
// snippet-start:[swift.sts.AssumeRole.command.runasync]
45+
func runAsync() async throws {
46+
// If credentials are specified, create a credential identity
47+
// resolver that uses them to authenticate. This identity will be used
48+
// to ask for permission to use the specified role.
49+
50+
var identityResolver: StaticAWSCredentialIdentityResolver? = nil
51+
52+
if accessKey != nil && secretKey != nil {
53+
do {
54+
identityResolver = try getIdentityResolver(accessKey: accessKey,
55+
secretKey: secretKey, sessionToken: sessionToken)
56+
} catch {
57+
print("ERROR: Unable to get identity resolver in runAsync:",
58+
dump(error))
59+
throw error
60+
}
61+
}
62+
63+
// Assume the role using the credentials provided on the command line,
64+
// or using the default credentials if none were specified.
65+
66+
do {
67+
// snippet-start: [swift.sts.AssumeRole]
68+
let credentials = try await assumeRole(identityResolver: identityResolver,
69+
roleArn: roleArn)
70+
do {
71+
identityResolver = try getIdentityResolver(
72+
accessKey: credentials.accessKey,
73+
secretKey: credentials.secret,
74+
sessionToken: credentials.sessionToken
75+
)
76+
} catch {
77+
print("ERROR: Unable to authenticate using provided options:",
78+
dump(error))
79+
throw error
80+
}
81+
// snippet-end: [swift.sts.AssumeRole]
82+
} catch {
83+
print("ERROR: Error assuming role in runAsync:", dump(error))
84+
throw AssumeRoleExampleError.assumeRoleFailed
85+
}
86+
87+
// Use the credential identity resolver to access AWS S3.
88+
89+
do {
90+
let names = try await getBucketNames(identityResolver: identityResolver)
91+
92+
print("Found \(names.count) buckets:")
93+
for name in names {
94+
print(" \(name)")
95+
}
96+
} catch {
97+
print("ERROR: Error getting bucket names in runAsync:", dump(error))
98+
throw error
99+
}
100+
}
101+
// snippet-end:[swift.sts.AssumeRole.command.runasync]
102+
}
103+
104+
/// An `Error` type used to return errors from the
105+
/// `assumeRole(identityResolver: roleArn:)` function.
106+
enum AssumeRoleExampleError: Error {
107+
/// An error indicating that the STS `AssumeRole` request failed.
108+
case assumeRoleFailed
109+
/// An error indicating that the returned credentials were missing
110+
/// required information.
111+
case incompleteCredentials
112+
/// An error indicating that no credentials were returned by `AssumeRole`.
113+
case missingCredentials
114+
115+
/// Return a human-readable explanation of the error.
116+
var errorDescription: String? {
117+
switch self {
118+
case .assumeRoleFailed:
119+
return "Unable to assume the specified role."
120+
case .incompleteCredentials:
121+
return "AWS STS returned incomplete credentials."
122+
case .missingCredentials:
123+
return "AWS STS did not return any credentials for the specified role."
124+
}
125+
}
126+
}
127+
128+
// snippet-start:[swift.sts.AssumeRole.assumeRole-function]
129+
/// Assume the specified role. If any kind of credential identity resolver is
130+
/// specified, that identity is adopted before assuming the role.
131+
///
132+
/// - Parameters:
133+
/// - identityResolver: Any kind of `AWSCredentialIdentityResolver`. If
134+
/// provided, this identity is adopted before attempting to assume the
135+
/// specified role.
136+
/// - roleArn: The ARN of the AWS role to assume.
137+
///
138+
/// - Throws: Re-throws STS errors. Also can throw any
139+
/// `AssumeRoleExampleError`.
140+
/// - Returns: An `AWSCredentialIdentity` containing the temporary credentials
141+
/// assigned.
142+
func assumeRole(identityResolver: (any AWSCredentialIdentityResolver)?,
143+
roleArn: String) async throws -> AWSCredentialIdentity {
144+
let stsConfiguration = try await STSClient.STSClientConfiguration(
145+
awsCredentialIdentityResolver: identityResolver
146+
)
147+
let stsClient = STSClient(config: stsConfiguration)
148+
149+
// Assume the role and return the assigned credentials.
150+
151+
// snippet-start: [swift.sts.sts.AssumeRole]
152+
let input = AssumeRoleInput(
153+
roleArn: roleArn,
154+
roleSessionName: "AssumeRole-Example"
155+
)
156+
157+
let output = try await stsClient.assumeRole(input: input)
158+
159+
guard let credentials = output.credentials else {
160+
throw AssumeRoleExampleError.missingCredentials
161+
}
162+
163+
guard let accessKey = credentials.accessKeyId,
164+
let secretKey = credentials.secretAccessKey,
165+
let sessionToken = credentials.sessionToken else {
166+
throw AssumeRoleExampleError.incompleteCredentials
167+
}
168+
// snippet-end: [swift.sts.sts.AssumeRole]
169+
170+
// Return an `AWSCredentialIdentity` object with the temporary
171+
// credentials.
172+
173+
let awsCredentials = AWSCredentialIdentity(
174+
accessKey: accessKey,
175+
secret: secretKey,
176+
sessionToken: sessionToken
177+
)
178+
return awsCredentials
179+
}
180+
// snippet-end:[swift.sts.AssumeRole.assumeRole-function]
181+
182+
/// Return an array containing the names of all available buckets using
183+
/// the specified credential identity resolver to authenticate.
184+
///
185+
/// - Parameter identityResolver: Any type of `AWSCredentialIdentityResolver`,
186+
/// used to authenticate and authorize the user for access to the bucket
187+
/// names.
188+
///
189+
/// - Throws: Re-throws errors from `ListBucketsPaginated`.
190+
///
191+
/// - Returns: An array of strings listing the buckets.
192+
func getBucketNames(identityResolver: (any AWSCredentialIdentityResolver)?)
193+
async throws -> [String] {
194+
do {
195+
// Get an S3Client with which to access Amazon S3.
196+
// snippet-start:[swift.sts.AssumeRole.use-resolver]
197+
let configuration = try await S3Client.S3ClientConfiguration(
198+
awsCredentialIdentityResolver: identityResolver
199+
)
200+
let client = S3Client(config: configuration)
201+
202+
// Use "Paginated" to get all the buckets. This lets the SDK handle
203+
// the 'continuationToken' in "ListBucketsOutput".
204+
let pages = client.listBucketsPaginated(
205+
input: ListBucketsInput( maxBuckets: 10)
206+
)
207+
// snippet-end:[swift.sts.AssumeRole.use-resolver]
208+
209+
// Get the bucket names.
210+
var bucketNames: [String] = []
211+
212+
do {
213+
for try await page in pages {
214+
guard let buckets = page.buckets else {
215+
print("Error: page is empty.")
216+
continue
217+
}
218+
219+
for bucket in buckets {
220+
bucketNames.append(bucket.name ?? "<unknown>")
221+
}
222+
}
223+
224+
return bucketNames
225+
} catch {
226+
print("ERROR: listBuckets:", dump(error))
227+
throw error
228+
}
229+
}
230+
}
231+
232+
/// Create a credential identity resolver using access key and secret access
233+
/// key.
234+
///
235+
/// - Parameters:
236+
/// - accessKey: A string containing the AWS access key ID.
237+
/// - secretKey: A string containing the AWS secret access key.
238+
/// - sessionToken: An optional string containing the session token.
239+
/// - Throws: Re-throws errors from AWSSDKIdentity.
240+
/// - Returns: A `StaticAWSCredentialIdentityResolver` that can be used when
241+
/// configuring service clients.
242+
func getIdentityResolver(accessKey: String?, secretKey: String?,
243+
sessionToken: String?)
244+
throws -> StaticAWSCredentialIdentityResolver? {
245+
246+
if accessKey == nil || secretKey == nil {
247+
return nil
248+
}
249+
250+
guard let accessKey = accessKey,
251+
let secretKey = secretKey else {
252+
return nil
253+
}
254+
255+
let credentials = AWSCredentialIdentity(
256+
accessKey: accessKey,
257+
secret: secretKey,
258+
sessionToken: sessionToken
259+
)
260+
261+
return try StaticAWSCredentialIdentityResolver(credentials)
262+
}
263+
264+
/// The program's asynchronous entry point.
265+
@main
266+
struct Main {
267+
static func main() async {
268+
let args = Array(CommandLine.arguments.dropFirst())
269+
270+
do {
271+
let command = try ExampleCommand.parse(args)
272+
try await command.runAsync()
273+
} catch {
274+
ExampleCommand.exit(withError: error)
275+
}
276+
}
277+
}

0 commit comments

Comments
 (0)