Skip to content

Commit b454f30

Browse files
committed
refactor authorization provider setup
motivation: more easily share authorization provider setup with libSwiftPM users changes: * move authorization providers setup from SwiftTool to a Worksapce configuraiton utility * adjust call sites and tests
1 parent ffd0759 commit b454f30

File tree

5 files changed

+249
-58
lines changed

5 files changed

+249
-58
lines changed

Sources/Basics/AuthorizationProvider.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ public struct KeychainAuthorizationProvider: AuthorizationProvider {
251251
// MARK: - Composite
252252

253253
public struct CompositeAuthorizationProvider: AuthorizationProvider {
254-
private let providers: [AuthorizationProvider]
254+
// internal for testing
255+
internal let providers: [AuthorizationProvider]
255256
private let observabilityScope: ObservabilityScope
256257

257258
public init(_ providers: AuthorizationProvider..., observabilityScope: ObservabilityScope) {

Sources/Commands/SwiftTool.swift

Lines changed: 12 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -577,63 +577,21 @@ public class SwiftTool {
577577
}
578578

579579
func getAuthorizationProvider() throws -> AuthorizationProvider? {
580-
var providers = [AuthorizationProvider]()
581-
582-
// netrc file has higher specificity than keychain so use it first
583-
try providers.append(contentsOf: self.getNetrcAuthorizationProviders())
584-
585-
#if canImport(Security)
586-
if self.options.keychain {
587-
providers.append(KeychainAuthorizationProvider(observabilityScope: self.observabilityScope))
588-
}
589-
#endif
590-
591-
return providers.isEmpty ? .none : CompositeAuthorizationProvider(providers, observabilityScope: self.observabilityScope)
592-
}
593-
594-
func getNetrcAuthorizationProviders() throws -> [NetrcAuthorizationProvider] {
595-
guard options.netrc else {
596-
return []
597-
}
598-
599-
var providers = [NetrcAuthorizationProvider]()
600-
601-
// Use custom .netrc file if specified, otherwise look for it within workspace and user's home directory.
602-
if let configuredPath = options.netrcFilePath {
603-
guard localFileSystem.exists(configuredPath) else {
604-
throw StringError("Did not find .netrc file at \(configuredPath).")
605-
}
606-
607-
providers.append(try NetrcAuthorizationProvider(path: configuredPath, fileSystem: localFileSystem))
580+
var authorization = Workspace.Configuration.Authorization.default
581+
if !options.netrc {
582+
authorization.netrc = .disabled
583+
} else if let configuredPath = options.netrcFilePath {
584+
authorization.netrc = .custom(configuredPath)
608585
} else {
609-
// User didn't tell us to use these .netrc files so be more lenient with errors
610-
func loadNetrcNoThrows(at path: AbsolutePath) -> NetrcAuthorizationProvider? {
611-
guard localFileSystem.exists(path) && localFileSystem.isReadable(path) else {
612-
return nil
613-
}
614-
615-
do {
616-
return try NetrcAuthorizationProvider(path: path, fileSystem: localFileSystem)
617-
} catch {
618-
self.observabilityScope.emit(warning: "Failed to load .netrc file at \(path). Error: \(error)")
619-
return nil
620-
}
621-
}
622-
623-
// Workspace's .netrc file should be consulted before user-global file
624-
// TODO: replace multiroot-data-file with explicit overrides
625-
if let localPath = try? (options.multirootPackageDataFile ?? self.getPackageRoot()).appending(component: ".netrc"),
626-
let localProvider = loadNetrcNoThrows(at: localPath) {
627-
providers.append(localProvider)
628-
}
629-
630-
let userHomePath = localFileSystem.homeDirectory.appending(component: ".netrc")
631-
if let userHomeProvider = loadNetrcNoThrows(at: userHomePath) {
632-
providers.append(userHomeProvider)
633-
}
586+
let rootPath = try options.multirootPackageDataFile ?? self.getPackageRoot()
587+
authorization.netrc = .workspaceAndUser(rootPath: rootPath)
634588
}
635589

636-
return providers
590+
#if canImport(Security)
591+
authorization.keychain = self.options.keychain ? .enabled : .disabled
592+
#endif
593+
594+
return try authorization.makeAuthorizationProvider(fileSystem: localFileSystem, observabilityScope: self.observabilityScope)
637595
}
638596

639597
/// Start redirecting the standard output stream to the standard error stream.

Sources/Workspace/WorkspaceConfiguration.swift

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,109 @@ extension Workspace {
209209
}
210210
}
211211

212+
// MARK: - Authorization
213+
214+
extension Workspace.Configuration {
215+
public struct Authorization {
216+
public var netrc: Netrc
217+
public var keychain: Keychain
218+
219+
public static var `default`: Self {
220+
#if canImport(Security)
221+
.init(netrc: .user, keychain: .enabled)
222+
#else
223+
.init(netrc: .user, keychain: .disabled)
224+
#endif
225+
}
226+
227+
public init(netrc: Netrc, keychain: Keychain) {
228+
self.netrc = netrc
229+
self.keychain = keychain
230+
}
231+
232+
public func makeAuthorizationProvider(fileSystem: FileSystem, observabilityScope: ObservabilityScope) throws -> AuthorizationProvider? {
233+
var providers = [AuthorizationProvider]()
234+
235+
switch self.netrc {
236+
case .custom(let path):
237+
guard fileSystem.exists(path) else {
238+
throw StringError("Did not find .netrc file at \(path).")
239+
}
240+
providers.append(try NetrcAuthorizationProvider(path: path, fileSystem: fileSystem))
241+
case .workspaceAndUser(let rootPath):
242+
// package/project "local" .netrc file, takes priority over user-level file
243+
let localPath = rootPath.appending(component: ".netrc")
244+
// user didn't tell us to explicitly use these .netrc files so be more lenient with errors
245+
if let localProvider = self.loadOptionalNetrc(fileSystem: fileSystem, path: localPath, observabilityScope: observabilityScope) {
246+
providers.append(localProvider)
247+
}
248+
249+
// user .netrc file (most typical)
250+
let userHomePath = fileSystem.homeDirectory.appending(component: ".netrc")
251+
// user didn't tell us to explicitly use these .netrc files so be more lenient with errors
252+
if let userHomeProvider = self.loadOptionalNetrc(fileSystem: fileSystem, path: userHomePath, observabilityScope: observabilityScope) {
253+
providers.append(userHomeProvider)
254+
}
255+
case .user:
256+
// user .netrc file (most typical)
257+
let userHomePath = fileSystem.homeDirectory.appending(component: ".netrc")
258+
259+
// user didn't tell us to explicitly use these .netrc files so be more lenient with errors
260+
if let userHomeProvider = self.loadOptionalNetrc(fileSystem: fileSystem, path: userHomePath, observabilityScope: observabilityScope) {
261+
providers.append(userHomeProvider)
262+
}
263+
case .disabled:
264+
// noop
265+
break
266+
}
267+
268+
switch self.keychain {
269+
case .enabled:
270+
#if canImport(Security)
271+
providers.append(KeychainAuthorizationProvider(observabilityScope: observabilityScope))
272+
#else
273+
throw InternalError("Keychain not supported on this platform")
274+
#endif
275+
case .disabled:
276+
// noop
277+
break
278+
}
279+
280+
return providers.isEmpty ? .none : CompositeAuthorizationProvider(providers, observabilityScope: observabilityScope)
281+
}
282+
283+
private func loadOptionalNetrc(
284+
fileSystem: FileSystem,
285+
path: AbsolutePath,
286+
observabilityScope: ObservabilityScope
287+
) -> NetrcAuthorizationProvider? {
288+
guard fileSystem.exists(path) && fileSystem.isReadable(path) else {
289+
return .none
290+
}
291+
292+
do {
293+
return try NetrcAuthorizationProvider(path: path, fileSystem: fileSystem)
294+
} catch {
295+
observabilityScope.emit(warning: "Failed to load .netrc file at \(path). Error: \(error)")
296+
return .none
297+
}
298+
}
299+
300+
public enum Netrc {
301+
case disabled
302+
case custom(AbsolutePath)
303+
case workspaceAndUser(rootPath: AbsolutePath)
304+
case user
305+
}
306+
307+
public enum Keychain {
308+
case disabled
309+
case enabled
310+
}
311+
}
312+
}
313+
314+
212315
// MARK: - Mirrors
213316

214317
extension Workspace.Configuration {

Tests/CommandsTests/SwiftToolTests.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ final class SwiftToolTests: CommandsTestCase {
8080
let options = try SwiftToolOptions.parse(["--package-path", packageRoot.pathString, "--netrc-file", customPath.pathString])
8181
let tool = try SwiftTool(options: options)
8282

83-
let netrcProviders = try tool.getNetrcAuthorizationProviders()
83+
let authorizationProvider = try tool.getAuthorizationProvider() as? CompositeAuthorizationProvider
84+
let netrcProviders = authorizationProvider?.providers.compactMap{ $0 as? NetrcAuthorizationProvider } ?? []
8485
XCTAssertEqual(netrcProviders.count, 1)
8586
XCTAssertEqual(netrcProviders.first.map { resolveSymlinks($0.path) }, resolveSymlinks(customPath))
8687

@@ -90,7 +91,7 @@ final class SwiftToolTests: CommandsTestCase {
9091

9192
// delete it
9293
try localFileSystem.removeFileTree(customPath)
93-
XCTAssertThrowsError(try tool.getNetrcAuthorizationProviders(), "error expected") { error in
94+
XCTAssertThrowsError(try tool.getAuthorizationProvider(), "error expected") { error in
9495
XCTAssertEqual(error as? StringError, StringError("Did not find .netrc file at \(customPath)."))
9596
}
9697
}
@@ -104,7 +105,8 @@ final class SwiftToolTests: CommandsTestCase {
104105
let options = try SwiftToolOptions.parse(["--package-path", packageRoot.pathString])
105106
let tool = try SwiftTool(options: options)
106107

107-
let netrcProviders = try tool.getNetrcAuthorizationProviders()
108+
let authorizationProvider = try tool.getAuthorizationProvider() as? CompositeAuthorizationProvider
109+
let netrcProviders = authorizationProvider?.providers.compactMap{ $0 as? NetrcAuthorizationProvider } ?? []
108110
XCTAssertTrue(netrcProviders.count >= 1) // This might include .netrc in user's home dir
109111
XCTAssertNotNil(netrcProviders.first { resolveSymlinks($0.path) == resolveSymlinks(localPath) })
110112

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
@testable import Basics
12+
import SPMTestSupport
13+
import TSCBasic
14+
import TSCUtility
15+
import Workspace
16+
import XCTest
17+
18+
final class AuthorizationConfigurationTests: XCTestCase {
19+
func testNetrcAuthorizationProviders() throws {
20+
let observability = ObservabilitySystem.makeForTesting()
21+
22+
// custom .netrc file
23+
24+
do {
25+
let fileSystem = InMemoryFileSystem()
26+
27+
let customPath = fileSystem.homeDirectory.appending(components: UUID().uuidString, "custom-netrc-file")
28+
try fileSystem.createDirectory(customPath.parentDirectory, recursive: true)
29+
try fileSystem.writeFileContents(customPath) {
30+
"machine mymachine.labkey.org login [email protected] password custom"
31+
}
32+
33+
let configuration = Workspace.Configuration.Authorization(netrc: .custom(customPath), keychain: .disabled)
34+
let authorizationProvider = try configuration.makeAuthorizationProvider(fileSystem: fileSystem, observabilityScope: observability.topScope) as? CompositeAuthorizationProvider
35+
let netrcProviders = authorizationProvider?.providers.compactMap{ $0 as? NetrcAuthorizationProvider }
36+
37+
XCTAssertEqual(netrcProviders?.count, 1)
38+
XCTAssertEqual(netrcProviders?.first.map { resolveSymlinks($0.path) }, resolveSymlinks(customPath))
39+
40+
let auth = authorizationProvider?.authentication(for: URL(string: "https://mymachine.labkey.org")!)
41+
XCTAssertEqual(auth?.user, "[email protected]")
42+
XCTAssertEqual(auth?.password, "custom")
43+
44+
// delete it
45+
try fileSystem.removeFileTree(customPath)
46+
XCTAssertThrowsError(try configuration.makeAuthorizationProvider(fileSystem: fileSystem, observabilityScope: observability.topScope), "error expected") { error in
47+
XCTAssertEqual(error as? StringError, StringError("Did not find .netrc file at \(customPath)."))
48+
}
49+
}
50+
51+
// user .netrc file
52+
53+
do {
54+
let fileSystem = InMemoryFileSystem()
55+
56+
let userPath = fileSystem.homeDirectory.appending(component: ".netrc")
57+
try fileSystem.createDirectory(userPath.parentDirectory, recursive: true)
58+
try fileSystem.writeFileContents(userPath) {
59+
"machine mymachine.labkey.org login [email protected] password user"
60+
}
61+
62+
let configuration = Workspace.Configuration.Authorization(netrc: .user, keychain: .disabled)
63+
let authorizationProvider = try configuration.makeAuthorizationProvider(fileSystem: fileSystem, observabilityScope: observability.topScope) as? CompositeAuthorizationProvider
64+
let netrcProviders = authorizationProvider?.providers.compactMap{ $0 as? NetrcAuthorizationProvider }
65+
66+
XCTAssertEqual(netrcProviders?.count, 1)
67+
XCTAssertEqual(netrcProviders?.first.map { resolveSymlinks($0.path) }, resolveSymlinks(userPath))
68+
69+
let auth = authorizationProvider?.authentication(for: URL(string: "https://mymachine.labkey.org")!)
70+
XCTAssertEqual(auth?.user, "[email protected]")
71+
XCTAssertEqual(auth?.password, "user")
72+
73+
// delete it
74+
do {
75+
try fileSystem.removeFileTree(userPath)
76+
let authorizationProvider = try configuration.makeAuthorizationProvider(fileSystem: fileSystem, observabilityScope: observability.topScope) as? CompositeAuthorizationProvider
77+
XCTAssertNil(authorizationProvider)
78+
}
79+
}
80+
81+
// workspace + user .netrc file
82+
83+
do {
84+
let fileSystem = InMemoryFileSystem()
85+
86+
let userPath = fileSystem.homeDirectory.appending(component: ".netrc")
87+
try fileSystem.createDirectory(userPath.parentDirectory, recursive: true)
88+
try fileSystem.writeFileContents(userPath) {
89+
"machine mymachine.labkey.org login [email protected] password user"
90+
}
91+
92+
let workspacePath = AbsolutePath.root.appending(components: UUID().uuidString, ".netrc")
93+
try fileSystem.createDirectory(workspacePath.parentDirectory, recursive: true)
94+
try fileSystem.writeFileContents(workspacePath) {
95+
"machine mymachine.labkey.org login [email protected] password workspace"
96+
}
97+
98+
let configuration = Workspace.Configuration.Authorization(netrc: .workspaceAndUser(rootPath: workspacePath.parentDirectory), keychain: .disabled)
99+
let authorizationProvider = try configuration.makeAuthorizationProvider(fileSystem: fileSystem, observabilityScope: observability.topScope) as? CompositeAuthorizationProvider
100+
let netrcProviders = authorizationProvider?.providers.compactMap{ $0 as? NetrcAuthorizationProvider }
101+
102+
XCTAssertEqual(netrcProviders?.count, 2)
103+
XCTAssertEqual(netrcProviders?.first.map { resolveSymlinks($0.path) }, resolveSymlinks(workspacePath))
104+
XCTAssertEqual(netrcProviders?.last.map { resolveSymlinks($0.path) }, resolveSymlinks(userPath))
105+
106+
let auth = authorizationProvider?.authentication(for: URL(string: "https://mymachine.labkey.org")!)
107+
XCTAssertEqual(auth?.user, "[email protected]")
108+
XCTAssertEqual(auth?.password, "workspace")
109+
110+
// delete workspace file
111+
do {
112+
try fileSystem.removeFileTree(workspacePath)
113+
let authorizationProvider = try configuration.makeAuthorizationProvider(fileSystem: fileSystem, observabilityScope: observability.topScope) as? CompositeAuthorizationProvider
114+
let auth = authorizationProvider?.authentication(for: URL(string: "https://mymachine.labkey.org")!)
115+
XCTAssertEqual(auth?.user, "[email protected]")
116+
XCTAssertEqual(auth?.password, "user")
117+
}
118+
119+
// delete user file
120+
do {
121+
try fileSystem.removeFileTree(userPath)
122+
let authorizationProvider = try configuration.makeAuthorizationProvider(fileSystem: fileSystem, observabilityScope: observability.topScope) as? CompositeAuthorizationProvider
123+
XCTAssertNil(authorizationProvider)
124+
}
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)