Skip to content
New issue

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

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

Already on GitHub? # to your account

Add token authentication support to SwiftPM #5838

Merged
merged 20 commits into from
Jan 6, 2023
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
73 changes: 65 additions & 8 deletions Sources/Basics/AuthorizationProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2021 Apple Inc. and the Swift project authors
// Copyright (c) 2021-2022 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 All @@ -22,6 +22,12 @@ public protocol AuthorizationProvider {
func authentication(for url: URL) -> (user: String, password: String)?
}

public protocol AuthorizationWriter {
func addOrUpdate(for url: URL, user: String, password: String, persist: Bool, callback: @escaping (Result<Void, Error>) -> Void)

func remove(for url: URL, callback: @escaping (Result<Void, Error>) -> Void)
}

public enum AuthorizationProviderError: Error {
case invalidURLHost
case notFound
Expand Down Expand Up @@ -53,23 +59,30 @@ private extension URL {

// MARK: - netrc

public struct NetrcAuthorizationProvider: AuthorizationProvider {
public class NetrcAuthorizationProvider: AuthorizationProvider, AuthorizationWriter {
// marked internal for testing
internal let path: AbsolutePath
private let fileSystem: FileSystem

private let cache = ThreadSafeKeyValueStore<String, (user: String, password: String)>()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is for adding credentials to memory so RegistryClient can read them for the login request.


public init(path: AbsolutePath, fileSystem: FileSystem) throws {
self.path = path
self.fileSystem = fileSystem
// validate file is okay at the time of initializing the provider
_ = try Self.load(fileSystem: fileSystem, path: path)
}

public mutating func addOrUpdate(for url: URL, user: String, password: String, callback: @escaping (Result<Void, Error>) -> Void) {
public func addOrUpdate(for url: URL, user: String, password: String, persist: Bool = true, callback: @escaping (Result<Void, Error>) -> Void) {
guard let machine = url.authenticationID else {
return callback(.failure(AuthorizationProviderError.invalidURLHost))
}

if !persist {
self.cache[machine] = (user, password)
return callback(.success(()))
}

// Same entry already exists, no need to add or update
let netrc = try? Self.load(fileSystem: self.fileSystem, path: self.path)
guard netrc?.machines.first(where: { $0.name.lowercased() == machine && $0.login == user && $0.password == password }) == nil else {
Expand All @@ -81,8 +94,8 @@ public struct NetrcAuthorizationProvider: AuthorizationProvider {
try self.fileSystem.withLock(on: self.path, type: .exclusive) {
let contents = try? self.fileSystem.readFileContents(self.path).contents
try self.fileSystem.writeFileContents(self.path) { stream in
// File does not exist yet
if let contents = contents {
// Write existing contents
if let contents = contents, !contents.isEmpty {
stream.write(contents)
stream.write("\n")
}
Expand All @@ -97,8 +110,15 @@ public struct NetrcAuthorizationProvider: AuthorizationProvider {
}
}

public func remove(for url: URL, callback: @escaping (Result<Void, Error>) -> Void) {
callback(.failure(AuthorizationProviderError.other("User must edit netrc file at \(self.path) manually to remove entries")))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't reliably update .netrc (e.g., we might delete comments by accident), so simply not support it for now.

}

public func authentication(for url: URL) -> (user: String, password: String)? {
self.machine(for: url).map { (user: $0.login, password: $0.password) }
if let machine = url.authenticationID, let cached = self.cache[machine] {
return cached
}
return self.machine(for: url).map { (user: $0.login, password: $0.password) }
}

private func machine(for url: URL) -> Basics.Netrc.Machine? {
Expand Down Expand Up @@ -133,17 +153,25 @@ public struct NetrcAuthorizationProvider: AuthorizationProvider {
// MARK: - Keychain

#if canImport(Security)
public struct KeychainAuthorizationProvider: AuthorizationProvider {
public class KeychainAuthorizationProvider: AuthorizationProvider, AuthorizationWriter {
private let observabilityScope: ObservabilityScope

private let cache = ThreadSafeKeyValueStore<String, (user: String, password: String)>()

public init(observabilityScope: ObservabilityScope) {
self.observabilityScope = observabilityScope
}

public func addOrUpdate(for url: URL, user: String, password: String, callback: @escaping (Result<Void, Error>) -> Void) {
public func addOrUpdate(for url: URL, user: String, password: String, persist: Bool = true, callback: @escaping (Result<Void, Error>) -> Void) {
guard let server = url.authenticationID else {
return callback(.failure(AuthorizationProviderError.invalidURLHost))
}

if !persist {
Copy link
Contributor

Choose a reason for hiding this comment

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

is this mainly for testing, or more broadly needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is general feature. login must test that the credentials work because persisting them.

self.cache[server] = (user, password)
return callback(.success(()))
}

guard let passwordData = password.data(using: .utf8) else {
return callback(.failure(AuthorizationProviderError.cannotEncodePassword))
}
Expand All @@ -159,11 +187,30 @@ public struct KeychainAuthorizationProvider: AuthorizationProvider {
}
}

public func remove(for url: URL, callback: @escaping (Result<Void, Error>) -> Void) {
guard let server = url.authenticationID else {
return callback(.failure(AuthorizationProviderError.invalidURLHost))
}

let `protocol` = self.protocol(for: url)

do {
try self.delete(server: server, protocol: `protocol`)
callback(.success(()))
} catch {
callback(.failure(error))
}
}

public func authentication(for url: URL) -> (user: String, password: String)? {
guard let server = url.authenticationID else {
return nil
}

if let cached = self.cache[server] {
return cached
}

do {
guard let existingItem = try self.search(server: server, protocol: self.protocol(for: url)) as? [String: Any],
let passwordData = existingItem[kSecValueData as String] as? Data,
Expand Down Expand Up @@ -216,6 +263,16 @@ public struct KeychainAuthorizationProvider: AuthorizationProvider {
return true
}

private func delete(server: String, protocol: CFString) throws {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server,
kSecAttrProtocol as String: `protocol`]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess else {
throw AuthorizationProviderError.other("Failed to delete credentials for server \(server) from keychain: status \(status)")
}
}

private func search(server: String, protocol: CFString) throws -> CFTypeRef? {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Basics/Netrc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public struct NetrcParser {
return try Self.parse(content)
}

/// Parses strnigified netrc content
/// Parses stringified netrc content
///
/// - Parameters:
/// - content: The content to parse
Expand Down
27 changes: 14 additions & 13 deletions Sources/CoreCommands/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,22 +151,28 @@ public struct SecurityOptions: ParsableArguments {
@Flag(name: .customLong("disable-sandbox"), help: "Disable using the sandbox when executing subprocesses")
public var shouldDisableSandbox: Bool = false

/// Whether to load .netrc files for authenticating with remote servers
/// when downloading binary artifacts or communicating with a registry.
/// Force usage of the netrc file even in cases where it is not allowed.
@Flag(name: .customLong("netrc"), help: "Use netrc file even in cases where other credential stores are preferred")
public var forceNetrc: Bool = false

/// Whether to load netrc files for authenticating with remote servers
/// when downloading binary artifacts. This has no effects on registry
/// communications.
@Flag(inversion: .prefixedEnableDisable,
exclusivity: .exclusive,
help: "Load credentials from a .netrc file")
help: "Load credentials from a netrc file")
public var netrc: Bool = true

/// The path to the .netrc file used when `netrc` is `true`.
/// The path to the netrc file used when `netrc` is `true`.
@Option(
name: .customLong("netrc-file"),
help: "Specify the .netrc file path.",
help: "Specify the netrc file path",
completion: .file())
public var netrcFilePath: AbsolutePath?

/// Whether to use keychain for authenticating with remote servers
/// when downloading binary artifacts or communicating with a registry.
/// when downloading binary artifacts. This has no effects on registry
/// communications.
#if canImport(Security)
@Flag(inversion: .prefixedEnableDisable,
exclusivity: .exclusive,
Expand Down Expand Up @@ -198,7 +204,6 @@ public struct ResolverOptions: ParsableArguments {
@Flag(name: .customLong("skip-update"), help: "Skip updating dependencies from their remote during a resolution")
public var skipDependencyUpdate: Bool = false


@Flag(help: "Define automatic transformation of source control based dependencies to registry based ones")
public var sourceControlToRegistryDependencyTransformation: SourceControlToRegistryDependencyTransformation = .swizzle

Expand Down Expand Up @@ -377,7 +382,7 @@ public struct BuildOptions: ParsableArguments {
case disableIndexStore
}

public enum TargetDependencyImportCheckingMode : String, Codable, ExpressibleByArgument {
public enum TargetDependencyImportCheckingMode: String, Codable, ExpressibleByArgument {
case none
case warn
case error
Expand All @@ -398,10 +403,8 @@ public struct LinkerOptions: ParsableArguments {
public var shouldLinkStaticSwiftStdlib: Bool = false
}


// MARK: - Extensions


extension BuildConfiguration: ExpressibleByArgument {
public init?(argument: String) {
self.init(rawValue: argument)
Expand Down Expand Up @@ -430,7 +433,6 @@ extension AbsolutePath: ExpressibleByArgument {
}
}


extension FingerprintCheckingMode: ExpressibleByArgument {
public init?(argument: String) {
self.init(rawValue: argument)
Expand Down Expand Up @@ -458,5 +460,4 @@ public extension Sanitizer {
}
}

extension BuildSystemProvider.Kind: ExpressibleByArgument {
}
extension BuildSystemProvider.Kind: ExpressibleByArgument {}
29 changes: 23 additions & 6 deletions Sources/CoreCommands/SwiftTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ public struct ToolWorkspaceConfiguration {
let wantsREPLProduct: Bool

public init(wantsMultipleTestProducts: Bool = false,
wantsREPLProduct: Bool = false) {
wantsREPLProduct: Bool = false)
{
yim-lee marked this conversation as resolved.
Show resolved Hide resolved
self.wantsMultipleTestProducts = wantsMultipleTestProducts
self.wantsREPLProduct = wantsREPLProduct
}
Expand Down Expand Up @@ -367,6 +368,7 @@ public final class SwiftTool {
emitDeprecatedConfigurationWarning: emitDeprecatedConfigurationWarning
),
authorizationProvider: self.getAuthorizationProvider(),
registryAuthorizationProvider: self.getRegistryAuthorizationProvider(),
configuration: .init(
skipDependenciesUpdates: options.resolver.skipDependencyUpdate,
prefetchBasedOnResolvedFile: options.resolver.shouldEnableResolverPrefetching,
Expand Down Expand Up @@ -428,8 +430,7 @@ public final class SwiftTool {
} else if let configuredPath = options.security.netrcFilePath {
authorization.netrc = .custom(configuredPath)
} else {
let rootPath = try options.locations.multirootPackageDataFile ?? self.getPackageRoot()
authorization.netrc = .workspaceAndUser(rootPath: rootPath)
authorization.netrc = .user
}

#if canImport(Security)
Expand All @@ -439,6 +440,22 @@ public final class SwiftTool {
return try authorization.makeAuthorizationProvider(fileSystem: self.fileSystem, observabilityScope: self.observabilityScope)
}

public func getRegistryAuthorizationProvider() throws -> AuthorizationProvider? {
var authorization = Workspace.Configuration.Authorization.default
if let configuredPath = options.security.netrcFilePath {
authorization.netrc = .custom(configuredPath)
} else {
authorization.netrc = .user
}

// Don't use OS credential store if user wants netrc
#if canImport(Security)
authorization.keychain = self.options.security.forceNetrc ? .disabled : .enabled
#endif

return try authorization.makeRegistryAuthorizationProvider(fileSystem: self.fileSystem, observabilityScope: self.observabilityScope)
}

/// Resolve the dependencies.
public func resolve() throws {
let workspace = try getActiveWorkspace()
Expand Down Expand Up @@ -524,8 +541,8 @@ public final class SwiftTool {

let buildParameters = try self.buildParameters()
let haveBuildManifestAndDescription =
self.fileSystem.exists(buildParameters.llbuildManifest) &&
self.fileSystem.exists(buildParameters.buildDescriptionPath)
self.fileSystem.exists(buildParameters.llbuildManifest) &&
self.fileSystem.exists(buildParameters.buildDescriptionPath)

if !haveBuildManifestAndDescription {
return false
Expand Down Expand Up @@ -867,6 +884,6 @@ extension BuildOptions.TargetDependencyImportCheckingMode {

public extension Basics.Diagnostic {
static func mutuallyExclusiveArgumentsError(arguments: [String]) -> Self {
.error(arguments.map{ "'\($0)'" }.spm_localizedJoin(type: .conjunction) + " are mutually exclusive")
.error(arguments.map { "'\($0)'" }.spm_localizedJoin(type: .conjunction) + " are mutually exclusive")
}
}
Loading