Skip to content

enhance configuraiton for authentication providers #3672

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

Merged
merged 1 commit into from
Aug 21, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions Sources/Basics/AuthorizationProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
This source file is part of the Swift.org open source project
Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

public protocol AuthorizationProvider {
func authentication(for url: URL) -> (user: String, password: String)?
}

extension AuthorizationProvider {
public func httpAuthorizationHeader(for url: URL) -> String? {
guard let (user, password) = self.authentication(for: url) else {
return nil
}
let authString = "\(user):\(password)"
guard let authData = authString.data(using: .utf8) else {
return nil
}
return "Basic \(authData.base64EncodedString())"
}
}
1 change: 1 addition & 0 deletions Sources/Basics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_library(Basics
AuthorizationProvider.swift
ByteString+Extensions.swift
ConcurrencyHelpers.swift
Dictionary+Extensions.swift
Expand Down
30 changes: 20 additions & 10 deletions Sources/Commands/SwiftTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -508,22 +508,32 @@ public class SwiftTool {
return newPath
}

func getAuthorizationProvider() throws -> AuthorizationProvider? {
// currently only single provider: netrc
return try self.getNetrcConfig()?.get()
}

func getNetrcConfig() throws -> Workspace.Configuration.Netrc? {
guard options.netrc || options.netrcFilePath != nil || options.netrcOptional else {
return .none
}

let netrcFilePath = try self.netrcFilePath()
return netrcFilePath.map { .init(path: $0, fileSystem: localFileSystem) }
}

private func netrcFilePath() throws -> AbsolutePath? {
guard options.netrc ||
options.netrcFilePath != nil ||
options.netrcOptional else { return nil }

let resolvedPath: AbsolutePath = options.netrcFilePath ?? AbsolutePath("\(NSHomeDirectory())/.netrc")
guard localFileSystem.exists(resolvedPath) else {
let netrcFilePath = options.netrcFilePath ?? localFileSystem.homeDirectory.appending(component: ".netrc")
guard localFileSystem.exists(netrcFilePath) else {
if !options.netrcOptional {
diagnostics.emit(error: "Cannot find mandatory .netrc file at \(resolvedPath.pathString). To make .netrc file optional, use --netrc-optional flag.")
diagnostics.emit(error: "Cannot find mandatory .netrc file at \(netrcFilePath). To make .netrc file optional, use --netrc-optional flag.")
throw ExitCode.failure
} else {
diagnostics.emit(warning: "Did not find optional .netrc file at \(resolvedPath.pathString).")
diagnostics.emit(warning: "Did not find optional .netrc file at \(netrcFilePath).")
return .none
}
}
return resolvedPath
return netrcFilePath
}

private func getSharedCacheDirectory() throws -> AbsolutePath? {
Expand Down Expand Up @@ -582,7 +592,7 @@ public class SwiftTool {
sharedConfigurationDirectory: sharedConfigurationDirectory
),
mirrors: self.getMirrorsConfig(sharedConfigurationDirectory: sharedConfigurationDirectory).mirrors,
netrcFilePath: self.netrcFilePath(),
authorizationProvider: self.getAuthorizationProvider(),
customManifestLoader: self.getManifestLoader(), // FIXME: doe we really need to customize it?
customRepositoryProvider: provider, // FIXME: doe we really need to customize it?
additionalFileRules: isXcodeBuildSystemEnabled ? FileRuleDescription.xcbuildFileTypes : FileRuleDescription.swiftpmFileTypes,
Expand Down
26 changes: 13 additions & 13 deletions Sources/Workspace/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public class Workspace {
/// The http client used for downloading binary artifacts.
fileprivate let httpClient: HTTPClient

fileprivate let netrcFilePath: AbsolutePath?
fileprivate let authorizationProvider: AuthorizationProvider?

/// The downloader used for unarchiving binary artifacts.
fileprivate let archiver: Archiver
Expand Down Expand Up @@ -239,7 +239,7 @@ public class Workspace {
/// - fileSystem: The file system to use.
/// - location: Workspace location configuration.
/// - mirrors: Dependencies mirrors.
/// - netrcFilePath: Path tot he netrc file.
/// - authorizationProvider: Provider of authentication information.
/// - customToolsVersion: A custom tools version.
/// - customManifestLoader: A custom manifest loader.
/// - customRepositoryManager: A custom repository manager.
Expand All @@ -249,15 +249,16 @@ public class Workspace {
/// - customArchiver: A custom archiver.
/// - customChecksumAlgorithm: A custom checksum algorithm.
/// - additionalFileRules: File rules to determine resource handling behavior.
/// - resolverUpdateEnabled: Enables the dependencies resolver automatic version update check, relying only on the resolved version file
/// - resolverPrefetchingEnabled: Enables the dependencies resolver prefetching based on the resolved version file
/// - resolverTracingEnabled:Enables the dependencies resolver tracing
/// - resolverUpdateEnabled: Enables the dependencies resolver automatic version update check. Enabled by default. When disabled the resolver relies only on the resolved version file
/// - resolverPrefetchingEnabled: Enables the dependencies resolver prefetching based on the resolved version file. Enabled by default..
/// - resolverTracingEnabled: Enables the dependencies resolver tracing. Disabled by default..
/// - sharedRepositoriesCacheEnabled: Enables the shared repository cache. Enabled by default..
/// - delegate: Delegate for workspace events
public init(
fileSystem: FileSystem,
location: Location,
mirrors: DependencyMirrors? = .none,
netrcFilePath: AbsolutePath? = .none,
authorizationProvider: AuthorizationProvider? = .none,
customToolsVersion: ToolsVersion? = .none,
customManifestLoader: ManifestLoaderProtocol? = .none,
customRepositoryManager: RepositoryManager? = .none,
Expand Down Expand Up @@ -311,7 +312,7 @@ public class Workspace {
self.location = location
self.delegate = delegate
self.mirrors = mirrors
self.netrcFilePath = netrcFilePath
self.authorizationProvider = authorizationProvider
self.manifestLoader = manifestLoader
self.currentToolsVersion = currentToolsVersion
self.toolsVersionLoader = toolsVersionLoader
Expand Down Expand Up @@ -380,7 +381,9 @@ public class Workspace {
sharedConfigurationDirectory: nil // legacy
),
mirrors: config?.mirrors,
netrcFilePath: netrcFilePath,
authorizationProvider: netrcFilePath.map {
try Configuration.Netrc(path: $0, fileSystem: fileSystem).get()
},
customToolsVersion: currentToolsVersion,
customManifestLoader: manifestLoader,
customRepositoryManager: repositoryManager,
Expand Down Expand Up @@ -1771,9 +1774,6 @@ extension Workspace {
let tempDiagnostics = DiagnosticsEngine()
let result = ThreadSafeArrayStore<ManagedArtifact>()

// FIXME: should this handle the error more gracefully?
let authProvider: AuthorizationProviding? = try? Netrc.load(fromFileAtPath: netrcFilePath).get()

// zip files to download
// stored in a thread-safe way as we may fetch more from "artifactbundleindex" files
let zipArtifacts = ThreadSafeArrayStore<RemoteArtifact>(artifacts.filter { $0.url.pathExtension.lowercased() == "zip" })
Expand All @@ -1787,7 +1787,7 @@ extension Workspace {
group.enter()
var request = HTTPClient.Request(method: .get, url: indexFile.url)
request.options.validResponseCodes = [200]
request.options.authorizationProvider = authProvider?.authorization(for:)
request.options.authorizationProvider = self.authorizationProvider?.httpAuthorizationHeader(for:)
self.httpClient.execute(request) { result in
defer { group.leave() }

Expand Down Expand Up @@ -1858,7 +1858,7 @@ extension Workspace {

group.enter()
var request = HTTPClient.Request.download(url: artifact.url, fileSystem: self.fileSystem, destination: archivePath)
request.options.authorizationProvider = authProvider?.authorization(for:)
request.options.authorizationProvider = self.authorizationProvider?.httpAuthorizationHeader(for:)
request.options.validResponseCodes = [200]
self.httpClient.execute(
request,
Expand Down
46 changes: 46 additions & 0 deletions Sources/Workspace/WorkspaceConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,52 @@ extension Workspace.Configuration {
}
}

// MARK: - Authentication

extension Workspace.Configuration {
public struct Netrc {
private let path: AbsolutePath
private let fileSystem: FileSystem

public init(path: AbsolutePath, fileSystem: FileSystem) {
self.path = path
self.fileSystem = fileSystem
}

public func get() throws -> AuthorizationProvider {
return try Self.load(self.path, fileSystem: self.fileSystem)
}

private static func load(_ path: AbsolutePath, fileSystem: FileSystem) throws -> AuthorizationProvider {
let netrc = try TSCUtility.Netrc.load(fromFileAtPath: path).get()
return NetrcAuthorizationProvider(netrc)
}

struct NetrcAuthorizationProvider: AuthorizationProvider {
private let underlying: TSCUtility.Netrc

init(_ underlying: TSCUtility.Netrc) {
self.underlying = underlying
}

func authentication(for url: Foundation.URL) -> (user: String, password: String)? {
return self.machine(for: url).map { (user: $0.login, password: $0.password) }
}

private func machine(for url: Foundation.URL) -> TSCUtility.Netrc.Machine? {
if let machine = self.underlying.machines.first(where: { $0.name.lowercased() == url.host?.lowercased() }) {
return machine
}
if let machine = self.underlying.machines.first(where: { $0.isDefault }) {
return machine
}
return .none
}
}
}
}


// MARK: - Deprecated 8/20201

extension Workspace {
Expand Down
35 changes: 35 additions & 0 deletions Tests/BasicsTests/AuthorizationProviderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Basics
import TSCBasic
import XCTest

final class AuthorizationProviderTests: XCTestCase {
func testBasicAPIs() {
struct TestProvider: AuthorizationProvider {
let map: [URL: (user: String, password: String)]

func authentication(for url: URL) -> (user: String, password: String)? {
return self.map[url]
}
}

let url = URL(string: "http://\(UUID().uuidString)")!
let user = UUID().uuidString
let password = UUID().uuidString
let provider = TestProvider(map: [url: (user: user, password: password)])

let auth = provider.authentication(for: url)
XCTAssertEqual(auth?.user, user)
XCTAssertEqual(auth?.password, password)
XCTAssertEqual(provider.httpAuthorizationHeader(for: url), "Basic " + "\(user):\(password)".data(using: .utf8)!.base64EncodedString())
}
}