Skip to content

Update auth logic #3789

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 3 commits into from
Oct 7, 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
2 changes: 1 addition & 1 deletion Sources/Basics/AuthorizationProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public extension AuthorizationProvider {
}
}

extension Foundation.URL {
private extension Foundation.URL {
var authenticationID: String? {
guard let host = host?.lowercased() else {
return nil
Expand Down
14 changes: 14 additions & 0 deletions Sources/Commands/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,20 @@ public struct SwiftToolOptions: ParsableArguments {
help: "Specify the .netrc file path.",
completion: .file())
var netrcFilePath: AbsolutePath?

/// Whether to use keychain for authenticating with remote servers
/// when downloading binary artifacts or communicating with a registry.
#if canImport(Security)
@Flag(inversion: .prefixedEnableDisable,
exclusivity: .exclusive,
help: "Search credentials in macOS keychain")
var keychain: Bool = true
#else
@Flag(inversion: .prefixedEnableDisable,
exclusivity: .exclusive,
help: .hidden)
var keychain: Bool = false
#endif

@Flag(name: .customLong("netrc"), help: .hidden)
var _deprecated_netrc: Bool = false
Expand Down
59 changes: 40 additions & 19 deletions Sources/Commands/SwiftTool.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
Copyright (c) 2014 - 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
Expand Down Expand Up @@ -497,39 +497,60 @@ public class SwiftTool {

func getAuthorizationProvider() throws -> AuthorizationProvider? {
var providers = [AuthorizationProvider]()

// netrc file has higher specificity than keychain so use it first
if let netrcConfigFile = try self.getNetrcConfigFile() {
providers.append(try NetrcAuthorizationProvider(path: netrcConfigFile, fileSystem: localFileSystem))
try providers.append(contentsOf: self.getNetrcAuthorizationProviders())

#if canImport(Security)
if self.options.keychain {
providers.append(KeychainAuthorizationProvider(observabilityScope: self.observabilityScope))
}

// TODO: add --no-keychain option to allow opt-out
//#if canImport(Security)
// providers.append(KeychainAuthorizationProvider(observabilityScope: self.observabilityScope))
//#endif
#endif

return providers.isEmpty ? .none : CompositeAuthorizationProvider(providers, observabilityScope: self.observabilityScope)
}

func getNetrcConfigFile() throws -> AbsolutePath? {
func getNetrcAuthorizationProviders() throws -> [NetrcAuthorizationProvider] {
Copy link
Contributor

@tomerd tomerd Oct 5, 2021

Choose a reason for hiding this comment

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

my expectation as a user would be a single netrc file loaded, with the following priority:

  • custom one specified via cli flag
  • project local
  • home directory

@yim-lee @mattt did you have a different semantic in mind?

Copy link
Contributor

Choose a reason for hiding this comment

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

For .netrc files, I think that's a good option, yeah.

Alternatively, we could say:

  • If you specify a --netrc-path, use only that
  • Otherwise, consult project local (./.netrc) and fallback to the user shared (~/.netrc)

This approach would give us the convenience of cascading, while being able to keep the behavior you described (for example, if we only wanted to load the local or shared .netrc file, we could pass it as --netrc-file).

The only downside is that this is more complex, and lacking sufficient diagnostics, could make it difficult for the user to understand where credentials are coming from.

Copy link
Contributor

@tomerd tomerd Oct 6, 2021

Choose a reason for hiding this comment

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

personally as a use I would expect the former (ie only load a single file, no fallback / merge with shared one). maybe we can investigate what other tools are doing?

Copy link
Contributor

Choose a reason for hiding this comment

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

also cc @neonichu @abertelrud for opinions

Copy link
Contributor

@tomerd tomerd Oct 6, 2021

Choose a reason for hiding this comment

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

looks like curl only supports single location, either one specified with a flag or ~/.netrc: https://everything.curl.dev/usingcurl/netrc#the-netrc-file-format

I wonder if this is a good policy for us to due to security reasons - so users don't want to accidentally check in the .netrc file

@robertlacroix @andyp-apple opinions

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this is a good policy for us to due to security reasons - so users don't want to accidentally check in the .netrc file

FWIW, we added .netrc to the default project template .gitignore file with #3511, which helps mitigate this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively, we could say:

  • If you specify a --netrc-path, use only that
  • Otherwise, consult project local (./.netrc) and fallback to the user shared (~/.netrc)

This is my understanding of the expected behavior described in SE-0292 and what I think I've implemented in this PR. My assumption is that local netrc would not have as many credentials as the user one (if both files exist), so for a given URL if credentials is not found in local netrc then looking for it in user netrc sounds reasonable to me.

It sounds like based on @tomerd and @mattt's comments that a single netrc file is fine. If that's the case I will remove this part of the changes and reduce the PR to simply add keychain auth.

Copy link
Contributor

@tomerd tomerd Oct 6, 2021

Choose a reason for hiding this comment

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

@yim-lee yes what you did is per spec, and correct. my question is more conceptual: if we got the spec right or if that would surprise users. there is also the option of not supporting project local at all IMO. in any case, I will leave it to @mattt to decide as he is the proposal author, unless we hear from the security folks something stronger.

Copy link
Contributor

Choose a reason for hiding this comment

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

@yim-lee @tomerd Since we have the spec from SE-0292 and this PR that implements the spec, I think we should go with that for now. If we find that this behavior is confusing or not what we want, we can amend this and update the spec / documentation in tandem.

Copy link
Contributor

Choose a reason for hiding this comment

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

sgtm

guard options.netrc else {
return .none
return []
}

var providers = [NetrcAuthorizationProvider]()

// Use custom .netrc file if specified, otherwise look for it within workspace and user's home directory.
if let configuredPath = options.netrcFilePath {
guard localFileSystem.exists(configuredPath) else {
throw StringError("Did not find .netrc file at \(configuredPath).")
}
return configuredPath
}

providers.append(try NetrcAuthorizationProvider(path: configuredPath, fileSystem: localFileSystem))
} else {
// User didn't tell us to use these .netrc files so be more lenient with errors
func loadNetrcNoThrows(at path: AbsolutePath) -> NetrcAuthorizationProvider? {
guard localFileSystem.exists(path) else { return nil }

do {
return try NetrcAuthorizationProvider(path: path, fileSystem: localFileSystem)
} catch {
self.observabilityScope.emit(warning: "Failed to load .netrc file at \(path). Error: \(error)")
return nil
}
}

// Workspace's .netrc file should be consulted before user-global file
// TODO: replace multiroot-data-file with explicit overrides
if let localPath = try? (options.multirootPackageDataFile ?? self.getPackageRoot()).appending(component: ".netrc"),
let localProvider = loadNetrcNoThrows(at: localPath) {
providers.append(localProvider)
}

// TODO: replace multiroot-data-file with explicit overrides
let localPath = try (options.multirootPackageDataFile ?? self.getPackageRoot()).appending(component: ".netrc")
if localFileSystem.exists(localPath) {
return localPath
let userHomePath = localFileSystem.homeDirectory.appending(component: ".netrc")
if let userHomeProvider = loadNetrcNoThrows(at: userHomePath) {
providers.append(userHomeProvider)
}
}

let userHomePath = localFileSystem.homeDirectory.appending(component: ".netrc")
return localFileSystem.exists(userHomePath) ? userHomePath : .none

return providers
}

private func getSharedCacheDirectory() throws -> AbsolutePath? {
Expand Down
4 changes: 2 additions & 2 deletions Sources/PackageCollections/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public protocol PackageCollectionsProtocol {
/// - reference: The package reference
/// - callback: The closure to invoke when result becomes available
// deprecated 9/21
@available(*, deprecated, message: "user getPackageMetadata(identity:) instead")
@available(*, deprecated, message: "use getPackageMetadata(identity:) instead")
func getPackageMetadata(
_ reference: PackageReference,
callback: @escaping (Result<PackageCollectionsModel.PackageMetadata, Error>) -> Void
Expand All @@ -129,7 +129,7 @@ public protocol PackageCollectionsProtocol {
/// processed collection will be used.
/// - callback: The closure to invoke when result becomes available
// deprecated 9/21
@available(*, deprecated, message: "user getPackageMetadata(identity:) instead")
@available(*, deprecated, message: "use getPackageMetadata(identity:) instead")
func getPackageMetadata(
_ reference: PackageReference,
collections: Set<PackageCollectionsModel.CollectionIdentifier>?,
Expand Down
2 changes: 1 addition & 1 deletion Tests/CommandsTests/RunToolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class RunToolTests: XCTestCase {
XCTAssert(stdout.contains("Swift Package Manager"), "got stdout:\n" + stdout)
}

func testUnkownProductAndArgumentPassing() throws {
func testUnknownProductAndArgumentPassing() throws {
fixture(name: "Miscellaneous/EchoExecutable") { path in

let result = try SwiftPMProduct.SwiftRun.executeProcess(
Expand Down
58 changes: 45 additions & 13 deletions Tests/CommandsTests/SwiftToolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,76 +8,108 @@
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Basics
@testable import Basics
@testable import Commands
import SPMTestSupport
import TSCBasic
import XCTest

final class SwiftToolTests: XCTestCase {
func testNetrcLocations() throws {
func testNetrcAuthorizationProviders() throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

<3

fixture(name: "DependencyResolution/External/XCFramework") { packageRoot in
let fs = localFileSystem

let localPath = packageRoot.appending(component: ".netrc")
let userHomePath = fs.homeDirectory.appending(component: ".netrc")

// custom .netrc file

do {
let customPath = fs.homeDirectory.appending(component: UUID().uuidString)
try fs.writeFileContents(customPath) {
"machine mymachine.labkey.org login [email protected] password custom"
}


let options = try SwiftToolOptions.parse(["--package-path", packageRoot.pathString, "--netrc-file", customPath.pathString])
let tool = try SwiftTool(options: options)
XCTAssertEqual(try tool.getNetrcConfigFile().map(resolveSymlinks), resolveSymlinks(customPath))

let netrcProviders = try tool.getNetrcAuthorizationProviders()
XCTAssertEqual(netrcProviders.count, 1)
XCTAssertEqual(netrcProviders.first.map { resolveSymlinks($0.path) }, resolveSymlinks(customPath))

let auth = try tool.getAuthorizationProvider()?.authentication(for: URL(string: "https://mymachine.labkey.org")!)
XCTAssertEqual(auth?.user, "[email protected]")
XCTAssertEqual(auth?.password, "custom")

// delete it
try localFileSystem.removeFileTree(customPath)
XCTAssertThrowsError(try tool.getNetrcConfigFile(), "error expected") { error in
XCTAssertThrowsError(try tool.getNetrcAuthorizationProviders(), "error expected") { error in
XCTAssertEqual(error as? StringError, StringError("Did not find .netrc file at \(customPath)."))
}
}

// local .netrc file

do {
let localPath = packageRoot.appending(component: ".netrc")
// make sure there isn't a user home one
try localFileSystem.removeFileTree(userHomePath)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The tests probably shouldn't touch user's .netrc file


try fs.writeFileContents(localPath) {
return "machine mymachine.labkey.org login [email protected] password local"
}

let options = try SwiftToolOptions.parse(["--package-path", packageRoot.pathString])
let tool = try SwiftTool(options: options)

let netrcProviders = try tool.getNetrcAuthorizationProviders()
XCTAssertEqual(netrcProviders.count, 1)
XCTAssertEqual(netrcProviders.first.map { resolveSymlinks($0.path) }, resolveSymlinks(localPath))

XCTAssertEqual(try tool.getNetrcConfigFile().map(resolveSymlinks), resolveSymlinks(localPath))
let auth = try tool.getAuthorizationProvider()?.authentication(for: URL(string: "https://mymachine.labkey.org")!)
XCTAssertEqual(auth?.user, "[email protected]")
XCTAssertEqual(auth?.password, "local")
}

// user .netrc file

do {
// make sure there isn't a local one
try localFileSystem.removeFileTree(packageRoot.appending(component: ".netrc"))
try localFileSystem.removeFileTree(localPath)

let userHomePath = fs.homeDirectory.appending(component: ".netrc")
try fs.writeFileContents(userHomePath) {
return "machine mymachine.labkey.org login [email protected] password user"
}

let options = try SwiftToolOptions.parse(["--package-path", packageRoot.pathString])
let tool = try SwiftTool(options: options)

XCTAssertEqual(try tool.getNetrcConfigFile().map(resolveSymlinks), resolveSymlinks(userHomePath))
let netrcProviders = try tool.getNetrcAuthorizationProviders()
XCTAssertEqual(netrcProviders.count, 1)
XCTAssertEqual(netrcProviders.first.map { resolveSymlinks($0.path) }, resolveSymlinks(userHomePath))

let auth = try tool.getAuthorizationProvider()?.authentication(for: URL(string: "https://mymachine.labkey.org")!)
XCTAssertEqual(auth?.user, "[email protected]")
XCTAssertEqual(auth?.password, "user")
}

// both local and user .netrc file
do {
try fs.writeFileContents(localPath) {
return "machine mymachine.labkey.org login [email protected] password local"
}
try fs.writeFileContents(userHomePath) {
return "machine mymachine.labkey.org login [email protected] password user"
}

let options = try SwiftToolOptions.parse(["--package-path", packageRoot.pathString])
let tool = try SwiftTool(options: options)

let netrcProviders = try tool.getNetrcAuthorizationProviders()
XCTAssertEqual(netrcProviders.count, 2)
XCTAssertEqual(netrcProviders.map { resolveSymlinks($0.path) }, [localPath, userHomePath].map(resolveSymlinks))

// local before user .netrc file
let auth = try tool.getAuthorizationProvider()?.authentication(for: URL(string: "https://mymachine.labkey.org")!)
XCTAssertEqual(auth?.user, "[email protected]")
XCTAssertEqual(auth?.password, "local")
}
}
}
}