Skip to content

Commit

Permalink
feat: Add token resolution to ECS credential provider (#1778)
Browse files Browse the repository at this point in the history
  • Loading branch information
dayaffe authored and jbelkins committed Oct 28, 2024
1 parent 606619a commit 8ccbac3
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import struct Foundation.URLComponents
/// A credential identity resolver that sources credentials from ECS container metadata
public struct ECSAWSCredentialIdentityResolver: AWSCredentialIdentityResolvedByCRT {
public let crtAWSCredentialIdentityResolver: AwsCommonRuntimeKit.CredentialsProvider
public let resolvedHost: String
public let resolvedPathAndQuery: String
public let resolvedAuthorizationToken: String?

/// Creates a credential identity resolver that resolves credentials from ECS container metadata.
/// ECS creds provider can be used to access creds via either relative uri to a fixed endpoint http://169.254.170.2,
/// or via a full uri specified by environment variables:
Expand Down Expand Up @@ -46,21 +50,27 @@ public struct ECSAWSCredentialIdentityResolver: AWSCredentialIdentityResolvedByC
let defaultHost = "169.254.170.2"
var host = defaultHost
var pathAndQuery = resolvedRelativeURI ?? ""
var resolvedAuthToken: String?

if let relative = resolvedRelativeURI {
pathAndQuery = relative
} else if let absolute = resolvedAbsoluteURI, let absoluteURL = URL(string: absolute) {
let (absoluteHost, absolutePathAndQuery) = try retrieveHostPathAndQuery(from: absoluteURL)
host = absoluteHost
pathAndQuery = absolutePathAndQuery
resolvedAuthToken = try resolveToken(authorizationToken, env)
} else {
throw HTTPClientError.pathCreationFailed(
"Failed to retrieve either relative or absolute URI! URI may be malformed."
)
}

self.resolvedHost = host
self.resolvedPathAndQuery = pathAndQuery
self.resolvedAuthorizationToken = resolvedAuthToken
self.crtAWSCredentialIdentityResolver = try AwsCommonRuntimeKit.CredentialsProvider(source: .ecs(
bootstrap: SDKDefaultIO.shared.clientBootstrap,
authToken: resolvedAuthToken,
pathAndQuery: pathAndQuery,
host: host
))
Expand Down Expand Up @@ -90,6 +100,26 @@ private func isValidAbsoluteURI(_ uri: String?) -> Bool {
return true
}

private func resolveToken(_ authorizationToken: String?, _ env: ProcessEnvironment) throws -> String? {
// Initialize token variable
var tokenFromFile: String?
if let tokenPath = env.environmentVariable(
key: "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"
) {
do {
// Load the token from the file
let tokenFilePath = URL(fileURLWithPath: tokenPath)
tokenFromFile = try String(contentsOf: tokenFilePath, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)
} catch {
throw ClientError.dataNotFound("Error reading the token file: \(error)")
}
}

// AWS_CONTAINER_AUTHORIZATION_TOKEN should only be used if AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE is not set
return authorizationToken ?? tokenFromFile ?? env.environmentVariable(key: "AWS_CONTAINER_AUTHORIZATION_TOKEN")
}

private struct ProcessEnvironment {
public init() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,44 @@ import protocol AWSClientRuntime.Environment
import struct AWSSDKIdentity.ECSAWSCredentialIdentityResolver

class ECSAWSCredentialIdentityResolverTests: XCTestCase {

override func setUp() {
super.setUp()

// Unset the environment variables before each test
unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
unsetenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")
unsetenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE")
unsetenv("AWS_CONTAINER_AUTHORIZATION_TOKEN")
}

override func tearDown() {
// Unset the environment variables after each test
unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
unsetenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")
unsetenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE")
unsetenv("AWS_CONTAINER_AUTHORIZATION_TOKEN")

super.tearDown()
}

func testGetCredentialsWithRelativeURI() async throws {
// relative uri is preferred over absolute uri so we shouldn't get thrown an error
XCTAssertNoThrow(try ECSAWSCredentialIdentityResolver(relativeURI: "subfolder/test.txt", absoluteURI: "invalid absolute uri"))
let resolver = try ECSAWSCredentialIdentityResolver(
relativeURI: "/subfolder/test.txt",
absoluteURI: "invalid absolute uri"
)
XCTAssertEqual(resolver.resolvedHost, "169.254.170.2")
XCTAssertEqual(resolver.resolvedPathAndQuery, "/subfolder/test.txt")
}

func testGetCredentialsWithAbsoluteURI() async throws {
XCTAssertNoThrow(try ECSAWSCredentialIdentityResolver(relativeURI: nil, absoluteURI: "http://www.example.com/subfolder/test.txt"))
let resolver = try ECSAWSCredentialIdentityResolver(
relativeURI: nil,
absoluteURI: "http://www.example.com/subfolder/test.txt"
)
XCTAssertEqual(resolver.resolvedHost, "www.example.com")
XCTAssertEqual(resolver.resolvedPathAndQuery, "/subfolder/test.txt")
}

func testGetCredentialsWithInvalidAbsoluteURI() async throws {
Expand All @@ -29,54 +60,80 @@ class ECSAWSCredentialIdentityResolverTests: XCTestCase {

func testGetCredentialsWithRelativeURIEnv() async throws {
// relative uri is preferred over absolute uri so we shouldn't get thrown an error
setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "subfolder/test.txt", 1)
unsetenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")
XCTAssertNoThrow(try ECSAWSCredentialIdentityResolver())
setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/subfolder/test.txt", 1)
let resolver = try ECSAWSCredentialIdentityResolver()
XCTAssertEqual(resolver.resolvedHost, "169.254.170.2")
XCTAssertEqual(resolver.resolvedPathAndQuery, "/subfolder/test.txt")
}

func testGetCredentialsWithAbsoluteURIEnv() async throws {
unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://www.example.com/subfolder/test.txt", 1)
XCTAssertNoThrow(try ECSAWSCredentialIdentityResolver())
let resolver = try ECSAWSCredentialIdentityResolver()
XCTAssertEqual(resolver.resolvedHost, "www.example.com")
XCTAssertEqual(resolver.resolvedPathAndQuery, "/subfolder/test.txt")
}

func testGetCredentialsWithInvalidAbsoluteURIEnv() async throws {
unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "test", 1)
XCTAssertThrowsError(try ECSAWSCredentialIdentityResolver())
}

func testGetCredentialsWithMissingURIEnv() async throws {
unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
unsetenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")
XCTAssertThrowsError(try ECSAWSCredentialIdentityResolver())
}
}

protocol EnvironmentProvider {
func environmentVariable(key: String) -> String?
}
func testGetCredentialsWithTokenFile() async throws {
// Simulating a token file

let tokenFilePath = Bundle.module.url(forResource: "test_token", withExtension: "txt")!.path

// Set the environment variable to point to the token file
setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", tokenFilePath, 1)
setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://www.example.com/subfolder/test.txt", 1)

class MockEnvironment: Environment, EnvironmentProvider {
let relativeURI: String?
let absoluteURI: String?
// Ensure the resolver correctly loads the token from the file
let resolver = try ECSAWSCredentialIdentityResolver()
XCTAssertEqual(resolver.resolvedAuthorizationToken, "sample-token")
XCTAssertEqual(resolver.resolvedHost, "www.example.com")
XCTAssertEqual(resolver.resolvedPathAndQuery, "/subfolder/test.txt")
}

func testGetCredentialsWithTokenEnv() async throws {
// Set the environment variable directly for the token
setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "env-token", 1)
setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://www.example.com/subfolder/test.txt", 1)

init(
relativeURI: String? = nil,
absoluteURI: String? = nil
) {
self.relativeURI = relativeURI
self.absoluteURI = absoluteURI
// Ensure the resolver correctly loads the token from the environment
let resolver = try ECSAWSCredentialIdentityResolver()
XCTAssertEqual(resolver.resolvedAuthorizationToken, "env-token")
XCTAssertEqual(resolver.resolvedHost, "www.example.com")
XCTAssertEqual(resolver.resolvedPathAndQuery, "/subfolder/test.txt")
}

func environmentVariable(key: String) -> String? {
switch key {
case "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI":
return self.relativeURI
case "AWS_CONTAINER_CREDENTIALS_FULL_URI":
return self.absoluteURI
default:
return nil
}
func testGetCredentialsWithDirectToken() async throws {
// Pass the token directly to the resolver
let resolver = try ECSAWSCredentialIdentityResolver(
absoluteURI: "http://www.example.com/subfolder/test.txt",
authorizationToken: "direct-token"
)

// Ensure the resolver correctly uses the passed token
XCTAssertEqual(resolver.resolvedAuthorizationToken, "direct-token")
XCTAssertEqual(resolver.resolvedHost, "www.example.com")
XCTAssertEqual(resolver.resolvedPathAndQuery, "/subfolder/test.txt")
}

func testTokenNotResolvedWithRelativeURI() async throws {
// Pass the token directly to the resolver
let resolver = try ECSAWSCredentialIdentityResolver(
relativeURI: "/test",
authorizationToken: "direct-token"
)

// Ensure the resolver correctly uses the passed token
// Authorization token is not used with relative URI
XCTAssertEqual(resolver.resolvedAuthorizationToken, nil)
XCTAssertEqual(resolver.resolvedHost, "169.254.170.2")
XCTAssertEqual(resolver.resolvedPathAndQuery, "/test")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sample-token

0 comments on commit 8ccbac3

Please sign in to comment.