diff --git a/Sources/tart/Commands/Login.swift b/Sources/tart/Commands/Login.swift index cd30e27..9f94587 100644 --- a/Sources/tart/Commands/Login.swift +++ b/Sources/tart/Commands/Login.swift @@ -9,13 +9,13 @@ struct Login: AsyncParsableCommand { var host: String @Option(help: "username") - var username: String + var username: String? @Flag(help: "password-stdin") var passwordStdin: Bool = false func validate() throws { - let usernameProvided = !username.isEmpty + let usernameProvided = username != nil let passwordProvided = passwordStdin if usernameProvided != passwordProvided { @@ -28,7 +28,7 @@ struct Login: AsyncParsableCommand { var user: String var password: String - if !username.isEmpty { + if let username = username { user = username password = readLine()! } else { diff --git a/Sources/tart/OCI/Authentication.swift b/Sources/tart/OCI/Authentication.swift new file mode 100644 index 0000000..515e749 --- /dev/null +++ b/Sources/tart/OCI/Authentication.swift @@ -0,0 +1,21 @@ +import Foundation + +protocol Authentication { + func header() -> (String, String) + func isValid() -> Bool +} + +struct BasicAuthentication: Authentication { + let user: String + let password: String + + func header() -> (String, String) { + let creds = Data("\(user):\(password)".utf8).base64EncodedString() + + return ("Authorization", "Basic \(creds)") + } + + func isValid() -> Bool { + true + } +} diff --git a/Sources/tart/OCI/Registry.swift b/Sources/tart/OCI/Registry.swift index 0c975e6..3cc91d6 100644 --- a/Sources/tart/OCI/Registry.swift +++ b/Sources/tart/OCI/Registry.swift @@ -25,7 +25,7 @@ extension HTTPClientResponse.Body { } } -struct TokenResponse: Decodable { +struct TokenResponse: Decodable, Authentication { let defaultIssuedAt = Date() let defaultExpiresIn = 60 @@ -65,10 +65,12 @@ struct TokenResponse: Decodable { } } - var isValid: Bool { - get { - Date() < tokenExpiresAt - } + func header() -> (String, String) { + ("Authorization", "Bearer \(token)") + } + + func isValid() -> Bool { + Date() < tokenExpiresAt } } @@ -83,7 +85,7 @@ class Registry { let namespace: String let credentialsProvider: CredentialsProvider - var currentAuthToken: TokenResponse? = nil + var currentAuthToken: Authentication? = nil init(urlComponents: URLComponents, namespace: String, @@ -245,7 +247,7 @@ class Registry { } // Invalidate token if it has expired - if currentAuthToken?.isValid == false { + if currentAuthToken?.isValid() == false { currentAuthToken = nil } @@ -266,6 +268,15 @@ class Registry { } let wwwAuthenticate = try WWWAuthenticate(rawHeaderValue: wwwAuthenticateRaw) + + if wwwAuthenticate.scheme == "Basic" { + if let (user, password) = try credentialsProvider.retrieve(host: baseURL.host!) { + currentAuthToken = BasicAuthentication(user: user, password: password) + } + + return + } + if wwwAuthenticate.scheme != "Bearer" { throw RegistryError.AuthFailed(why: "WWW-Authenticate header's authentication scheme " + "\"\(wwwAuthenticate.scheme)\" is unsupported, expected \"Bearer\" scheme") @@ -316,7 +327,8 @@ class Registry { var request = request if let token = currentAuthToken { - request.headers.add(name: "Authorization", value: "Bearer \(token.token)") + let (name, value) = token.header() + request.headers.add(name: name, value: value) } return try await httpClient.execute(request, deadline: .distantFuture) diff --git a/Tests/TartTests/TokenResponseTests.swift b/Tests/TartTests/TokenResponseTests.swift index eaf2672..f961c05 100644 --- a/Tests/TartTests/TokenResponseTests.swift +++ b/Tests/TartTests/TokenResponseTests.swift @@ -11,7 +11,7 @@ final class TokenResponseTests: XCTestCase { let expectedTokenExpiresAtRange = Date()...Date().addingTimeInterval(60) XCTAssertTrue(expectedTokenExpiresAtRange.contains(tokenResponse.tokenExpiresAt)) - XCTAssertTrue(tokenResponse.isValid) + XCTAssertTrue(tokenResponse.isValid()) } func testExpirationBasic() throws { @@ -23,9 +23,9 @@ final class TokenResponseTests: XCTestCase { let expectedTokenExpiresAtRange = Date()...Date().addingTimeInterval(2) XCTAssertTrue(expectedTokenExpiresAtRange.contains(tokenResponse.tokenExpiresAt)) - XCTAssertTrue(tokenResponse.isValid) + XCTAssertTrue(tokenResponse.isValid()) _ = XCTWaiter.wait(for: [expectation(description: "Wait 3 seconds for the token to become invalid")], timeout: 2) - XCTAssertFalse(tokenResponse.isValid) + XCTAssertFalse(tokenResponse.isValid()) } func testExpirationWithIssuedAt() throws { @@ -33,6 +33,6 @@ final class TokenResponseTests: XCTestCase { let tokenResponse = try TokenResponse.parse(fromData: tokenResponseRaw) XCTAssertEqual(Date(timeIntervalSince1970: 3600), tokenResponse.tokenExpiresAt) - XCTAssertFalse(tokenResponse.isValid) + XCTAssertFalse(tokenResponse.isValid()) } }