-
-
Notifications
You must be signed in to change notification settings - Fork 60
Expand file tree
/
Copy pathConfigFileLoader.swift
More file actions
318 lines (290 loc) · 14.3 KB
/
Copy pathConfigFileLoader.swift
File metadata and controls
318 lines (290 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
//===----------------------------------------------------------------------===//
//
// This source file is part of the Soto for AWS open source project
//
// Copyright (c) 2017-2020 the Soto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Soto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import INIParser
import Logging
import NIOCore
import NIOPosix
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// Load settings from AWS credentials and profile configuration files
/// https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html
enum ConfigFileLoader {
static let defaultProfileConfigPath = "~/.aws/config"
static let defaultProfile = "default"
static let defaultCredentialsPath = "~/.aws/credentials"
/// Specific type of credentials loaded from disk
enum SharedCredentials {
case staticCredential(credential: StaticCredential)
case assumeRole(roleArn: String, sessionName: String, region: Region?, sourceCredentialProvider: CredentialProviderFactory)
}
/// Credentials file – The credentials and config file are updated when you run the command aws configure. The credentials file is located
/// at `~/.aws/credentials` on Linux or macOS, or at C:\Users\USERNAME\.aws\credentials on Windows. This file can contain the credential
/// details for the default profile and any named profiles.
struct ProfileCredentials: Equatable {
let accessKey: String?
let secretAccessKey: String?
let sessionToken: String?
let roleArn: String?
let roleSessionName: String?
let sourceProfile: String?
let credentialSource: CredentialSource?
}
/// The credentials and config file are updated when you run the command aws configure. The config file is located at `~/.aws/config` on Linux
/// or macOS, or at C:\Users\USERNAME\.aws\config on Windows. This file contains the configuration settings for the default profile and any named profiles.
struct ProfileConfig: Equatable {
let region: Region?
let roleArn: String?
let roleSessionName: String?
let sourceProfile: String?
let credentialSource: CredentialSource?
}
/// Profile credential source `credential_source`
///
/// Used within Amazon EC2 instances or EC2 containers to specify where the AWS CLI can find credentials to use to assume the role you
/// specified with the `role_arn` parameter. You cannot specify both `source_profile` and `credential_source` in the same profile.
enum CredentialSource: String, Equatable {
case environment = "Environment"
case ec2Instance = "Ec2InstanceMetadata"
case ecsContainer = "EcsContainer"
}
/// Errors occurring when loading credentials and profile configuration
/// - invalidCredentialFile: If credentials could not be loaded from disk because of invalid configuration or syntax
/// - missingProfile: If the profile requested was not found
/// - missingAccessKeyId: If the access key ID was not found
/// - missingSecretAccessKey: If the secret access key was not found
enum ConfigFileError: Error, Equatable {
case invalidCredentialFile
case missingProfile(String)
case missingAccessKeyId
case missingSecretAccessKey
}
// MARK: - File IO
/// Load credentials from disk
/// - Parameters:
/// - credentialsFilePath: file path for AWS credentials file
/// - configFilePath: file path for AWS config file
/// - profile: named profile to load
/// - context: credential provider factory context
/// - Returns: Promise of SharedCredentials
static func loadSharedCredentials(
credentialsFilePath: String,
configFilePath: String,
profile: String,
threadPool: NIOThreadPool = .singleton
) async throws -> SharedCredentials {
let fileIO = NonBlockingFileIO(threadPool: threadPool)
let credentialsByteBuffer: ByteBuffer
do {
// Load credentials file
credentialsByteBuffer = try await self.loadFile(
path: credentialsFilePath,
fileIO: fileIO
)
} catch {
// Throw `.noProvider` error if credential file cannot be loaded
throw CredentialProviderError.noProvider
}
let configByteBuffer: ByteBuffer?
do {
// Load profile config file
configByteBuffer = try await self.loadFile(
path: configFilePath,
fileIO: fileIO
)
} catch {
configByteBuffer = nil
}
return try self.parseSharedCredentials(from: credentialsByteBuffer, configByteBuffer: configByteBuffer, for: profile)
}
/// Load a file from disk without blocking the current thread
/// - Parameters:
/// - path: path for the file to load
/// - eventLoop: event loop to run everything on
/// - fileIO: non-blocking file IO
/// - Returns: Event loop future with file contents in a byte-buffer
static func loadFile(path: String, fileIO: NonBlockingFileIO) async throws -> ByteBuffer {
let path = self.expandTildeInFilePath(path)
return try await fileIO.withFileRegion(path: path) { fileRegion in
try await fileIO.read(fileHandle: fileRegion.fileHandle, byteCount: fileRegion.readableBytes, allocator: ByteBufferAllocator())
}
}
// MARK: - Byte Buffer parsing (INIParser)
/// Parse credentials from files (passed in as byte-buffers).
/// This method ensures credentials are valid according to AWS documentation.
///
/// Credentials file settings have precedence over profile configuration settings.
/// https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-precedence
///
/// - Parameters:
/// - credentialsBuffer: contents of AWS shared credentials file (usually `~/.aws/credentials`)
/// - configByteBuffer: contents of AWS profile configuration file (usually `~/.aws/config`)
/// - profile: named profile to load (optional)
/// - Returns: Parsed SharedCredentials
static func parseSharedCredentials(
from credentialsByteBuffer: ByteBuffer,
configByteBuffer: ByteBuffer?,
for profile: String
) throws -> SharedCredentials {
let config = try configByteBuffer.flatMap { try self.parseProfileConfig(from: $0, for: profile) }
let credentials = try parseCredentials(from: credentialsByteBuffer, for: profile, sourceProfile: config?.sourceProfile)
// If `role_arn` is defined, check for source profile or credential source
if let roleArn = credentials.roleArn ?? config?.roleArn {
let sessionName = credentials.roleSessionName ?? config?.roleSessionName ?? UUID().uuidString
let region = config?.region ?? .useast1
// If `source_profile` is defined, temporary credentials must be loaded via STS AssumeRole operation
if let _ = credentials.sourceProfile ?? config?.sourceProfile {
guard let accessKey = credentials.accessKey else {
throw ConfigFileError.missingAccessKeyId
}
guard let secretAccessKey = credentials.secretAccessKey else {
throw ConfigFileError.missingSecretAccessKey
}
let provider: CredentialProviderFactory = .static(
accessKeyId: accessKey,
secretAccessKey: secretAccessKey,
sessionToken: credentials.sessionToken
)
return .assumeRole(roleArn: roleArn, sessionName: sessionName, region: region, sourceCredentialProvider: provider)
}
// If `credental_source` is defined, temporary credentials must be loaded from source
else if let credentialSource = credentials.credentialSource ?? config?.credentialSource {
let provider: CredentialProviderFactory
switch credentialSource {
case .environment:
provider = .environment
case .ec2Instance:
provider = .ec2
case .ecsContainer:
provider = .ecs
}
return .assumeRole(roleArn: roleArn, sessionName: sessionName, region: region, sourceCredentialProvider: provider)
}
// Invalid configuration
throw ConfigFileError.invalidCredentialFile
}
// Return static credentials
guard let accessKey = credentials.accessKey else {
throw ConfigFileError.missingAccessKeyId
}
guard let secretAccessKey = credentials.secretAccessKey else {
throw ConfigFileError.missingSecretAccessKey
}
let credential = StaticCredential(accessKeyId: accessKey, secretAccessKey: secretAccessKey, sessionToken: credentials.sessionToken)
return .staticCredential(credential: credential)
}
/// Parse profile configuraton from a file (passed in as byte-buffer), usually `~/.aws/config`
///
/// - Parameters:
/// - byteBuffer: contents of the file to parse
/// - profile: AWS named profile to load (usually `default`)
/// - Returns: Combined profile settings
static func parseProfileConfig(from byteBuffer: ByteBuffer, for profile: String) throws -> ProfileConfig? {
guard let content = byteBuffer.getString(at: 0, length: byteBuffer.readableBytes),
let parser = try? INIParser(content)
else {
throw ConfigFileError.invalidCredentialFile
}
// The credentials file uses a different naming format than the CLI config file for named profiles. Include
// the prefix word "profile" only when configuring a named profile in the config file. Do not use the word
// profile when creating an entry in the credentials file.
// https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
let loadedProfile = profile == Self.defaultProfile ? profile : "profile \(profile)"
// Gracefully fail if there is no configuration for the given profile
guard let settings = parser.sections[loadedProfile] else {
return nil
}
// All values are optional for profile configuration
return ProfileConfig(
region: settings["region"].flatMap(Region.init(awsRegionName:)),
roleArn: settings["role_arn"],
roleSessionName: settings["role_session_name"],
sourceProfile: settings["source_profile"],
credentialSource: settings["credential_source"].flatMap(CredentialSource.init(rawValue:))
)
}
/// Parse profile credentials from a file (passed in as byte-buffer), usually `~/.aws/credentials`
///
/// - Parameters:
/// - byteBuffer: contents of the file to parse
/// - profile: AWS named profile to load (usually `default`)
/// - sourceProfile: specifies a named profile with long-term credentials that the AWS CLI can use to assume a role that you specified with the `role_arn` parameter.
/// - Returns: Combined profile credentials
static func parseCredentials(from byteBuffer: ByteBuffer, for profile: String, sourceProfile: String?) throws -> ProfileCredentials {
guard let content = byteBuffer.getString(at: 0, length: byteBuffer.readableBytes),
let parser = try? INIParser(content)
else {
throw ConfigFileError.invalidCredentialFile
}
guard let settings = parser.sections[profile] else {
throw ConfigFileError.missingProfile(profile)
}
var accessKey = settings["aws_access_key_id"]
var secretAccessKey = settings["aws_secret_access_key"]
var sessionToken = settings["aws_session_token"]
// If a source profile is indicated, load credentials for STS Assume Role operation.
// Credentials file settings have precedence over profile configuration settings.
if let sourceProfile = settings["source_profile"] ?? sourceProfile {
guard let sourceSettings = parser.sections[sourceProfile] else {
throw ConfigFileError.missingProfile(sourceProfile)
}
accessKey = sourceSettings["aws_access_key_id"]
secretAccessKey = sourceSettings["aws_secret_access_key"]
sessionToken = sourceSettings["aws_session_token"]
}
return ProfileCredentials(
accessKey: accessKey,
secretAccessKey: secretAccessKey,
sessionToken: sessionToken,
roleArn: settings["role_arn"],
roleSessionName: settings["role_session_name"],
sourceProfile: sourceProfile ?? settings["source_profile"],
credentialSource: settings["credential_source"].flatMap(CredentialSource.init(rawValue:))
)
}
// MARK: - Path Expansion
static func expandTildeInFilePath(_ filePath: String) -> String {
#if os(Linux)
// We don't want to add more dependencies on Foundation than needed.
// For this reason we get the expanded filePath on Linux from libc.
// Since `wordexp` and `wordfree` are not available on iOS we stay
// with NSString on Darwin.
return filePath.withCString { ptr -> String in
var wexp = wordexp_t()
guard wordexp(ptr, &wexp, 0) == 0, let we_wordv = wexp.we_wordv else {
return filePath
}
defer {
wordfree(&wexp)
}
guard let resolved = we_wordv[0], let pth = String(cString: resolved, encoding: .utf8) else {
return filePath
}
return pth
}
#elseif os(macOS)
// can not use wordexp on macOS because for sandboxed application wexp.we_wordv == nil
guard let home = getpwuid(getuid())?.pointee.pw_dir,
let homePath = String(cString: home, encoding: .utf8)
else {
return filePath
}
return filePath.starts(with: "~") ? homePath + filePath.dropFirst() : filePath
#else
return NSString(string: filePath).expandingTildeInPath
#endif
}
}