Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add token resolution to ECS credential provider #1778

Merged
merged 6 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,35 @@ 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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of an abundance of caution, should we unset these during test teardown too?

}


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 +51,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)

// 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)

class MockEnvironment: Environment, EnvironmentProvider {
let relativeURI: String?
let absoluteURI: String?
// 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 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"
)

init(
relativeURI: String? = nil,
absoluteURI: String? = nil
) {
self.relativeURI = relativeURI
self.absoluteURI = absoluteURI
// 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 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 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
Loading