|
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,17 @@ 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 | + case docker |
| 48 | + case unixDomainSocket |
| 49 | + } |
| 50 | + |
| 51 | + @Option(name: .customLong("allow-network-connections")) |
| 52 | + var allowNetworkConnections: NetworkPermission = .none |
41 | 53 | }
|
42 | 54 |
|
43 | 55 | @OptionGroup()
|
@@ -116,38 +128,73 @@ struct PluginCommand: SwiftCommand {
|
116 | 128 | // 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 | 129 | let outputDir = pluginsDir.appending(component: "outputs")
|
118 | 130 |
|
| 131 | + var allowNetworkConnections = [SandboxNetworkPermission.init(options.allowNetworkConnections)] |
119 | 132 | // Determine the set of directories under which plugins are allowed to write. We always include the output directory.
|
120 | 133 | var writableDirectories = [outputDir]
|
121 | 134 | if options.allowWritingToPackageDirectory {
|
122 | 135 | writableDirectories.append(package.path)
|
123 | 136 | }
|
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) |
| 137 | + |
| 138 | + // If the plugin requires permissions, we ask the user for approval. |
| 139 | + if case .command(_, let permissions) = plugin.capability { |
| 140 | + try permissions.forEach { |
| 141 | + let permissionString: String |
| 142 | + let reasonString: String |
| 143 | + let remedyOption: String |
| 144 | + |
| 145 | + switch $0 { |
| 146 | + case .writeToPackageDirectory(let reason): |
| 147 | + guard !options.allowWritingToPackageDirectory else { return } // permission already granted |
| 148 | + permissionString = "write to the package directory" |
| 149 | + reasonString = reason |
| 150 | + remedyOption = "--allow-writing-to-package-directory" |
| 151 | + case .allowNetworkConnections(let scope, let reason): |
| 152 | + guard scope != .none else { return } // no need to prompt |
| 153 | + guard options.allowNetworkConnections != .init(scope) else { return } // permission already granted |
| 154 | + |
| 155 | + switch scope { |
| 156 | + case .all, .local: |
| 157 | + let portsString = scope.ports.isEmpty ? "on all ports" : "on ports: \(scope.ports.map { "\($0)" }.joined(separator: ", "))" |
| 158 | + permissionString = "allow \(scope.label) network connections \(portsString)" |
| 159 | + case .docker, .unixDomainSocket: |
| 160 | + permissionString = "allow \(scope.label) connections" |
| 161 | + case .none: |
| 162 | + permissionString = "" // should not be reached |
142 | 163 | }
|
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")) |
| 164 | + |
| 165 | + reasonString = reason |
| 166 | + remedyOption = "--allow-network-connections \(PluginCommand.PluginOptions.NetworkPermission.init(scope).defaultValueDescription)" |
| 167 | + } |
| 168 | + |
| 169 | + let problem = "Plugin ‘\(plugin.name)’ wants permission to \(permissionString)." |
| 170 | + let reason = "Stated reason: “\(reasonString)”." |
| 171 | + if swiftTool.outputStream.isTTY { |
| 172 | + // We can ask the user directly, so we do so. |
| 173 | + let query = "Allow this plugin to \(permissionString)?" |
| 174 | + swiftTool.outputStream.write("\(problem)\n\(reason)\n\(query) (yes/no) ".utf8) |
| 175 | + swiftTool.outputStream.flush() |
| 176 | + let answer = readLine(strippingNewline: true) |
| 177 | + // Throw an error if we didn't get permission. |
| 178 | + if answer?.lowercased() != "yes" { |
| 179 | + throw StringError("Plugin was denied permission to \(permissionString).") |
147 | 180 | }
|
| 181 | + } else { |
| 182 | + // We can't ask the user, so emit an error suggesting passing the flag. |
| 183 | + let remedy = "Use `\(remedyOption)` to allow this." |
| 184 | + throw StringError([problem, reason, remedy].joined(separator: "\n")) |
| 185 | + } |
| 186 | + |
| 187 | + switch $0 { |
| 188 | + case .writeToPackageDirectory: |
| 189 | + // Otherwise append the directory to the list of allowed ones. |
| 190 | + writableDirectories.append(package.path) |
| 191 | + case .allowNetworkConnections(let scope, _): |
| 192 | + allowNetworkConnections.append(.init(scope)) |
| 193 | + break |
148 | 194 | }
|
149 | 195 | }
|
150 | 196 | }
|
| 197 | + |
151 | 198 | for pathString in options.additionalAllowedWritableDirectories {
|
152 | 199 | writableDirectories.append(try AbsolutePath(validating: pathString, relativeTo: swiftTool.originalWorkingDirectory))
|
153 | 200 | }
|
@@ -187,6 +234,7 @@ struct PluginCommand: SwiftCommand {
|
187 | 234 | accessibleTools: accessibleTools,
|
188 | 235 | writableDirectories: writableDirectories,
|
189 | 236 | readOnlyDirectories: readOnlyDirectories,
|
| 237 | + allowNetworkConnections: allowNetworkConnections, |
190 | 238 | pkgConfigDirectories: swiftTool.options.locations.pkgConfigDirectories,
|
191 | 239 | fileSystem: swiftTool.fileSystem,
|
192 | 240 | observabilityScope: swiftTool.observabilityScope,
|
@@ -223,3 +271,39 @@ extension PluginCommandIntent {
|
223 | 271 | }
|
224 | 272 | }
|
225 | 273 | }
|
| 274 | + |
| 275 | +extension SandboxNetworkPermission { |
| 276 | + init(_ scope: PluginNetworkPermissionScope) { |
| 277 | + switch scope { |
| 278 | + case .none: self = .none |
| 279 | + case .local(let ports): self = .local(ports: ports) |
| 280 | + case .all(let ports): self = .all(ports: ports) |
| 281 | + case .docker: self = .docker |
| 282 | + case .unixDomainSocket: self = .unixDomainSocket |
| 283 | + } |
| 284 | + } |
| 285 | +} |
| 286 | + |
| 287 | +extension PluginCommand.PluginOptions.NetworkPermission { |
| 288 | + fileprivate init(_ scope: PluginNetworkPermissionScope) { |
| 289 | + switch scope { |
| 290 | + case .unixDomainSocket: self = .unixDomainSocket |
| 291 | + case .docker: self = .docker |
| 292 | + case .none: self = .none |
| 293 | + case .all: self = .all |
| 294 | + case .local: self = .local |
| 295 | + } |
| 296 | + } |
| 297 | +} |
| 298 | + |
| 299 | +extension SandboxNetworkPermission { |
| 300 | + init(_ permission: PluginCommand.PluginOptions.NetworkPermission) { |
| 301 | + switch permission { |
| 302 | + case .none: self = .none |
| 303 | + case .local: self = .local(ports: []) |
| 304 | + case .all: self = .all(ports: []) |
| 305 | + case .docker: self = .docker |
| 306 | + case .unixDomainSocket: self = .unixDomainSocket |
| 307 | + } |
| 308 | + } |
| 309 | +} |
0 commit comments