Skip to content

Commit

Permalink
prompt for unsigned or untrusted packages if requested (#6258)
Browse files Browse the repository at this point in the history
motivation: support the prompt configuration for handling unsigned or untrusted packages

changes:
* handle prompt request via swift-tool delegate
* generic input system for swift-tool observability handler
  • Loading branch information
tomerd authored Mar 10, 2023
1 parent b1e729b commit 5a7b7fe
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 36 deletions.
43 changes: 41 additions & 2 deletions Sources/Commands/ToolWorkspaceDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Basics
import CoreCommands
import Dispatch
import class Foundation.NSLock
import struct Foundation.URL
import OrderedCollections
import PackageGraph
import PackageModel
Expand Down Expand Up @@ -47,15 +48,18 @@ class ToolWorkspaceDelegate: WorkspaceDelegate {

private let outputHandler: (String, Bool) -> Void
private let progressHandler: (Int64, Int64, String?) -> Void
private let inputHandler: (String, (String?) -> Void) -> Void

init(
observabilityScope: ObservabilityScope,
outputHandler: @escaping (String, Bool) -> Void,
progressHandler: @escaping (Int64, Int64, String?) -> Void
progressHandler: @escaping (Int64, Int64, String?) -> Void,
inputHandler: @escaping (String, (String?) -> Void) -> Void
) {
self.observabilityScope = observabilityScope
self.outputHandler = outputHandler
self.progressHandler = progressHandler
self.inputHandler = inputHandler
}

func willFetchPackage(package: PackageIdentity, packageLocation: String?, fetchDetails: PackageFetchDetails) {
Expand Down Expand Up @@ -171,6 +175,36 @@ class ToolWorkspaceDelegate: WorkspaceDelegate {
self.progressHandler(step, total, "Downloading \(artifacts)")
}

// registry signature handlers

func onUnsignedRegistryPackage(registryURL: URL, package: PackageModel.PackageIdentity, version: TSCUtility.Version, completion: (Bool) -> Void) {
self.inputHandler("\(package) @ \(version) from \(registryURL) is unsigned. okay to proceed? (yes/no) ") { response in
switch response?.lowercased() {
case "yes":
completion(true) // continue
case "no":
completion(false) // stop resolution
default:
self.outputHandler("invalid response: '\(response ?? "")'", false)
completion(false)
}
}
}

func onUntrustedRegistryPackage(registryURL: URL, package: PackageModel.PackageIdentity, version: TSCUtility.Version, completion: (Bool) -> Void) {
self.inputHandler("\(package) @ \(version) from \(registryURL) is signed with an untrusted certificate. okay to proceed? (yes/no) ") { response in
switch response?.lowercased() {
case "yes":
completion(true) // continue
case "no":
completion(false) // stop resolution
default:
self.outputHandler("invalid response: '\(response ?? "")'", false)
completion(false)
}
}
}

// noop

func willLoadManifest(packageIdentity: PackageIdentity, packagePath: AbsolutePath, url: String, version: Version?, packageKind: PackageReference.Kind) {}
Expand All @@ -184,7 +218,12 @@ class ToolWorkspaceDelegate: WorkspaceDelegate {
public extension _SwiftCommand {
var workspaceDelegateProvider: WorkspaceDelegateProvider {
return {
ToolWorkspaceDelegate(observabilityScope: $0, outputHandler: $1, progressHandler: $2)
ToolWorkspaceDelegate(
observabilityScope: $0,
outputHandler: $1,
progressHandler: $2,
inputHandler: $3
)
}
}

Expand Down
6 changes: 4 additions & 2 deletions Sources/CoreCommands/SwiftTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public struct ToolWorkspaceConfiguration {
public typealias WorkspaceDelegateProvider = (
_ observabilityScope: ObservabilityScope,
_ outputHandler: @escaping (String, Bool) -> Void,
_ progressHandler: @escaping (Int64, Int64, String?) -> Void
_ progressHandler: @escaping (Int64, Int64, String?) -> Void,
_ inputHandler: @escaping (String, (String?) -> Void) -> Void
) -> WorkspaceDelegate
public typealias WorkspaceLoaderProvider = (_ fileSystem: FileSystem, _ observabilityScope: ObservabilityScope)
-> WorkspaceLoader
Expand Down Expand Up @@ -437,7 +438,8 @@ public final class SwiftTool {
let delegate = self.workspaceDelegateProvider(
self.observabilityScope,
self.observabilityHandler.print,
self.observabilityHandler.progress
self.observabilityHandler.progress,
self.observabilityHandler.prompt
)
let isXcodeBuildSystemEnabled = self.options.build.buildSystem == .xcode
let workspace = try Workspace(
Expand Down
19 changes: 19 additions & 0 deletions Sources/CoreCommands/SwiftToolObservabilityHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ public struct SwiftToolObservabilityHandler: ObservabilityHandlerProvider {
self.outputHandler.outputStream
}

// prompt for user input
func prompt(_ message: String, completion: (String?) -> Void) {
self.outputHandler.prompt(message: message, completion: completion)
}

func wait(timeout: DispatchTime) {
self.outputHandler.wait(timeout: timeout)
}
Expand Down Expand Up @@ -120,6 +125,20 @@ public struct SwiftToolObservabilityHandler: ObservabilityHandlerProvider {
}
}

// to read input from user
func prompt(message: String, completion: (String?) -> Void) {
guard self.outputStream.isTTY else {
return completion(.none)
}
let answer = self.queue.sync {
self.progressAnimation.clear()
self.outputStream.write(message.utf8)
self.outputStream.flush()
return readLine(strippingNewline: true)
}
completion(answer)
}

func wait(timeout: DispatchTime) {
switch self.sync.wait(timeout: timeout) {
case .success:
Expand Down
14 changes: 7 additions & 7 deletions Sources/PackageRegistry/RegistryClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public final class RegistryClient: Cancellable {
authorizationProvider: AuthorizationProvider? = .none,
customHTTPClient: LegacyHTTPClient? = .none,
customArchiverProvider: ((FileSystem) -> Archiver)? = .none,
delegate: Delegate? = .none
delegate: Delegate?
) {
self.configuration = configuration

Expand Down Expand Up @@ -1980,11 +1980,11 @@ private struct RegistryClientSignatureValidationDelegate: SignatureValidation.De
completion: (Bool) -> Void
) {
if let underlying = self.underlying {
// TODO: record the responses locally
underlying.onUnsigned(registry: registry, package: package, version: version, completion: completion)
} else {
// TODO: consider this to be false by default
completion(true)
// true == continue resolution
// false == stop dependency resolution
completion(false)
}
}

Expand All @@ -1995,11 +1995,11 @@ private struct RegistryClientSignatureValidationDelegate: SignatureValidation.De
completion: (Bool) -> Void
) {
if let underlying = self.underlying {
// TODO: record the responses locally
underlying.onUntrusted(registry: registry, package: package, version: version, completion: completion)
} else {
// TODO: consider this to be false by default
completion(true)
// true == continue resolution
// false == stop dependency resolution
completion(false)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/PackageRegistryTool/PackageRegistryTool+Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ extension SwiftPackageRegistryTool {
fingerprintCheckingMode: .strict,
signingEntityStorage: .none,
signingEntityCheckingMode: .strict,
authorizationProvider: authorizationProvider
authorizationProvider: authorizationProvider,
delegate: .none
)

// Try logging in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ extension SwiftPackageRegistryTool {
fingerprintCheckingMode: .strict,
signingEntityStorage: .none,
signingEntityCheckingMode: .strict,
authorizationProvider: authorizationProvider
authorizationProvider: authorizationProvider,
delegate: .none
)

// step 1: publishing configuration
Expand Down
3 changes: 2 additions & 1 deletion Sources/SPMTestSupport/MockRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public class MockRegistry {
signingEntityCheckingMode: .strict,
authorizationProvider: .none,
customHTTPClient: LegacyHTTPClient(handler: self.httpHandler),
customArchiverProvider: { fileSystem in MockRegistryArchiver(fileSystem: fileSystem) }
customArchiverProvider: { fileSystem in MockRegistryArchiver(fileSystem: fileSystem) },
delegate: .none
)
}

Expand Down
60 changes: 59 additions & 1 deletion Sources/Workspace/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,25 @@ public protocol WorkspaceDelegate: AnyObject {
func downloadingBinaryArtifact(from url: String, bytesDownloaded: Int64, totalBytesToDownload: Int64?)
/// The workspace finished downloading all binary artifacts.
func didDownloadAllBinaryArtifacts()

// handlers for unsigned and untrusted registry based dependencies
func onUnsignedRegistryPackage(registryURL: URL, package: PackageModel.PackageIdentity, version: TSCUtility.Version, completion: (Bool) -> Void)
func onUntrustedRegistryPackage(registryURL: URL, package: PackageModel.PackageIdentity, version: TSCUtility.Version, completion: (Bool) -> Void)
}

// FIXME: default implementation until the feature is stable, at which point we should remove this and force the clients to implement
extension WorkspaceDelegate {
public func onUnsignedRegistryPackage(registryURL: URL, package: PackageModel.PackageIdentity, version: TSCUtility.Version, completion: (Bool) -> Void) {
// true == continue resolution
// false == stop dependency resolution
completion(true)
}

public func onUntrustedRegistryPackage(registryURL: URL, package: PackageModel.PackageIdentity, version: TSCUtility.Version, completion: (Bool) -> Void) {
// true == continue resolution
// false == stop dependency resolution
completion(true)
}
}

private class WorkspaceRepositoryManagerDelegate: RepositoryManager.Delegate {
Expand Down Expand Up @@ -166,6 +185,44 @@ private struct WorkspaceRegistryDownloadsManagerDelegate: RegistryDownloadsManag
}
}

private struct WorkspaceRegistryClientDelegate: RegistryClient.Delegate {
private unowned let workspaceDelegate: Workspace.Delegate?

init(workspaceDelegate: Workspace.Delegate?) {
self.workspaceDelegate = workspaceDelegate
}

func onUnsigned(registry: PackageRegistry.Registry, package: PackageModel.PackageIdentity, version: TSCUtility.Version, completion: (Bool) -> Void) {
if let delegate = self.workspaceDelegate {
delegate.onUnsignedRegistryPackage(
registryURL: registry.url,
package: package,
version: version,
completion: completion
)
} else {
// true == continue resolution
// false == stop dependency resolution
completion(true)
}
}

func onUntrusted(registry: PackageRegistry.Registry, package: PackageModel.PackageIdentity, version: TSCUtility.Version, completion: (Bool) -> Void) {
if let delegate = self.workspaceDelegate {
delegate.onUntrustedRegistryPackage(
registryURL: registry.url,
package: package,
version: version,
completion: completion
)
} else {
// true == continue resolution
// false == stop dependency resolution
completion(true)
}
}
}

private struct WorkspaceDependencyResolverDelegate: DependencyResolverDelegate {
private unowned let workspaceDelegate: Workspace.Delegate
private let resolving = ThreadSafeKeyValueStore<PackageIdentity, Bool>()
Expand Down Expand Up @@ -622,7 +679,8 @@ public class Workspace {
fingerprintCheckingMode: FingerprintCheckingMode.map(configuration.fingerprintCheckingMode),
signingEntityStorage: signingEntities,
signingEntityCheckingMode: SigningEntityCheckingMode.map(configuration.signingEntityCheckingMode),
authorizationProvider: registryAuthorizationProvider
authorizationProvider: registryAuthorizationProvider,
delegate: WorkspaceRegistryClientDelegate(workspaceDelegate: delegate)
)

let registryDownloadsManager = RegistryDownloadsManager(
Expand Down
12 changes: 10 additions & 2 deletions Tests/CommandsTests/SwiftToolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,18 @@ extension SwiftTool {
options: options,
toolWorkspaceConfiguration: .init(),
workspaceDelegateProvider: {
ToolWorkspaceDelegate(observabilityScope: $0, outputHandler: $1, progressHandler: $2)
ToolWorkspaceDelegate(
observabilityScope: $0,
outputHandler: $1,
progressHandler: $2,
inputHandler: $3
)
},
workspaceLoaderProvider: {
XcodeWorkspaceLoader(fileSystem: $0, observabilityScope: $1)
XcodeWorkspaceLoader(
fileSystem: $0,
observabilityScope: $1
)
})
}
}
Loading

0 comments on commit 5a7b7fe

Please sign in to comment.