|
11 | 11 | //===----------------------------------------------------------------------===//
|
12 | 12 |
|
13 | 13 | import ArgumentParser
|
| 14 | +import Basics |
14 | 15 | import CoreCommands
|
15 | 16 | import Dispatch
|
16 | 17 | import PackageGraph
|
@@ -38,6 +39,15 @@ struct PluginCommand: SwiftCommand {
|
38 | 39 | @Option(name: .customLong("allow-writing-to-directory"),
|
39 | 40 | help: "Allow the plugin to write to an additional directory")
|
40 | 41 | var additionalAllowedWritableDirectories: [String] = []
|
| 42 | + |
| 43 | + enum NetworkPermission: String, EnumerableFlag, ExpressibleByArgument { |
| 44 | + case none |
| 45 | + case local |
| 46 | + case all |
| 47 | + } |
| 48 | + |
| 49 | + @Option(name: .customLong("allow-network-connections")) |
| 50 | + var allowNetworkConnections: NetworkPermission = .none |
41 | 51 | }
|
42 | 52 |
|
43 | 53 | @OptionGroup()
|
@@ -116,38 +126,64 @@ struct PluginCommand: SwiftCommand {
|
116 | 126 | // The `outputs` directory contains subdirectories for each combination of package and command plugin. Each usage of a plugin has an output directory that is writable by the plugin, where it can write additional files, and to which it can configure tools to write their outputs, etc.
|
117 | 127 | let outputDir = pluginsDir.appending(component: "outputs")
|
118 | 128 |
|
| 129 | + var allowNetworkConnections = SandboxNetworkPermission.init(options.allowNetworkConnections) |
119 | 130 | // Determine the set of directories under which plugins are allowed to write. We always include the output directory.
|
120 | 131 | var writableDirectories = [outputDir]
|
121 | 132 | if options.allowWritingToPackageDirectory {
|
122 | 133 | writableDirectories.append(package.path)
|
123 | 134 | }
|
124 |
| - else { |
125 |
| - // If the plugin requires write permission but it wasn't provided, we ask the user for approval. |
126 |
| - if case .command(_, let permissions) = plugin.capability { |
127 |
| - for case PluginPermission.writeToPackageDirectory(let reason) in permissions { |
128 |
| - let problem = "Plugin ‘\(plugin.name)’ wants permission to write to the package directory." |
129 |
| - let reason = "Stated reason: “\(reason)”." |
130 |
| - if swiftTool.outputStream.isTTY { |
131 |
| - // We can ask the user directly, so we do so. |
132 |
| - let query = "Allow this plugin to write to the package directory?" |
133 |
| - swiftTool.outputStream.write("\(problem)\n\(reason)\n\(query) (yes/no) ".utf8) |
134 |
| - swiftTool.outputStream.flush() |
135 |
| - let answer = readLine(strippingNewline: true) |
136 |
| - // Throw an error if we didn't get permission. |
137 |
| - if answer?.lowercased() != "yes" { |
138 |
| - throw ValidationError("Plugin was denied permission to write to the package directory.") |
139 |
| - } |
140 |
| - // Otherwise append the directory to the list of allowed ones. |
141 |
| - writableDirectories.append(package.path) |
142 |
| - } |
143 |
| - else { |
144 |
| - // We can't ask the user, so emit an error suggesting passing the flag. |
145 |
| - let remedy = "Use `--allow-writing-to-package-directory` to allow this." |
146 |
| - throw ValidationError([problem, reason, remedy].joined(separator: "\n")) |
| 135 | + |
| 136 | + // If the plugin requires permissions, we ask the user for approval. |
| 137 | + if case .command(_, let permissions) = plugin.capability { |
| 138 | + try permissions.forEach { |
| 139 | + let permissionString: String |
| 140 | + let reasonString: String |
| 141 | + let remedyOption: String |
| 142 | + |
| 143 | + switch $0 { |
| 144 | + case .writeToPackageDirectory(let reason): |
| 145 | + guard !options.allowWritingToPackageDirectory else { return } // permission already granted |
| 146 | + permissionString = "write to the package directory" |
| 147 | + reasonString = reason |
| 148 | + remedyOption = "--allow-writing-to-package-directory" |
| 149 | + case .allowNetworkConnections(let scope, let reason): |
| 150 | + guard scope != .none else { return } // no need to prompt |
| 151 | + guard options.allowNetworkConnections != scope else { return } // permission already granted |
| 152 | + let portsString = scope.ports.isEmpty ? "on all ports" : "on ports: \(scope.ports)" |
| 153 | + permissionString = "allow \(scope.label) network connections \(portsString)" |
| 154 | + reasonString = reason |
| 155 | + remedyOption = "--allow-network-connections \(scope.label)" |
| 156 | + } |
| 157 | + |
| 158 | + let problem = "Plugin ‘\(plugin.name)’ wants permission to \(permissionString)." |
| 159 | + let reason = "Stated reason: “\(reasonString)”." |
| 160 | + if swiftTool.outputStream.isTTY { |
| 161 | + // We can ask the user directly, so we do so. |
| 162 | + let query = "Allow this plugin to \(permissionString)?" |
| 163 | + swiftTool.outputStream.write("\(problem)\n\(reason)\n\(query) (yes/no) ".utf8) |
| 164 | + swiftTool.outputStream.flush() |
| 165 | + let answer = readLine(strippingNewline: true) |
| 166 | + // Throw an error if we didn't get permission. |
| 167 | + if answer?.lowercased() != "yes" { |
| 168 | + throw StringError("Plugin was denied permission to \(permissionString).") |
147 | 169 | }
|
| 170 | + } else { |
| 171 | + // We can't ask the user, so emit an error suggesting passing the flag. |
| 172 | + let remedy = "Use `\(remedyOption)` to allow this." |
| 173 | + throw StringError([problem, reason, remedy].joined(separator: "\n")) |
| 174 | + } |
| 175 | + |
| 176 | + switch $0 { |
| 177 | + case .writeToPackageDirectory: |
| 178 | + // Otherwise append the directory to the list of allowed ones. |
| 179 | + writableDirectories.append(package.path) |
| 180 | + case .allowNetworkConnections(let scope, _): |
| 181 | + allowNetworkConnections = .init(scope) |
| 182 | + break |
148 | 183 | }
|
149 | 184 | }
|
150 | 185 | }
|
| 186 | + |
151 | 187 | for pathString in options.additionalAllowedWritableDirectories {
|
152 | 188 | writableDirectories.append(try AbsolutePath(validating: pathString, relativeTo: swiftTool.originalWorkingDirectory))
|
153 | 189 | }
|
@@ -187,6 +223,7 @@ struct PluginCommand: SwiftCommand {
|
187 | 223 | accessibleTools: accessibleTools,
|
188 | 224 | writableDirectories: writableDirectories,
|
189 | 225 | readOnlyDirectories: readOnlyDirectories,
|
| 226 | + allowNetworkConnections: allowNetworkConnections, |
190 | 227 | pkgConfigDirectories: swiftTool.options.locations.pkgConfigDirectories,
|
191 | 228 | fileSystem: swiftTool.fileSystem,
|
192 | 229 | observabilityScope: swiftTool.observabilityScope,
|
@@ -223,3 +260,37 @@ extension PluginCommandIntent {
|
223 | 260 | }
|
224 | 261 | }
|
225 | 262 | }
|
| 263 | + |
| 264 | +extension SandboxNetworkPermission { |
| 265 | + init(_ scope: PluginNetworkPermissionScope) { |
| 266 | + switch scope { |
| 267 | + case .none: self = .none |
| 268 | + case .local(let ports): self = .local(ports: ports) |
| 269 | + case .all(let ports): self = .all(ports: ports) |
| 270 | + } |
| 271 | + } |
| 272 | +} |
| 273 | + |
| 274 | +extension PluginCommand.PluginOptions.NetworkPermission { |
| 275 | + public static func == (lhs: PluginCommand.PluginOptions.NetworkPermission, rhs: PluginNetworkPermissionScope) -> Bool { |
| 276 | + switch rhs { |
| 277 | + case .none: return lhs == .none |
| 278 | + case .local: return lhs == .local |
| 279 | + case .all: return lhs == .all |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + public static func != (lhs: PluginCommand.PluginOptions.NetworkPermission, rhs: PluginNetworkPermissionScope) -> Bool { |
| 284 | + return !(lhs == rhs) |
| 285 | + } |
| 286 | +} |
| 287 | + |
| 288 | +extension SandboxNetworkPermission { |
| 289 | + init(_ permission: PluginCommand.PluginOptions.NetworkPermission) { |
| 290 | + switch permission { |
| 291 | + case .none: self = .none |
| 292 | + case .local: self = .local(ports: []) |
| 293 | + case .all: self = .all(ports: []) |
| 294 | + } |
| 295 | + } |
| 296 | +} |
0 commit comments