src/config: add Configuration class to encapsulate the extension settings

Configuration's get returns the 'go' section of the vscode settings.
This wraps the vscode.WorkspaceConfiguration object that would be returned
by vscode.workspace.getConfiguration, but this prevents it from returning
values from the workspace/workspaceFolder level settings if the queried
key is security-sensitive - e.g. use of workspace level settings from
the untrusted repository may result in arbitrary binary execution.

This CL does not use the new Configuration from the extension yet.

For golang/vscode-go#1094

Change-Id: Ia75389032dfaec300506b26ab839b173ff9e5557
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/283253
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..b12ba51
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,65 @@
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import vscode = require('vscode');
+
+const SECURITY_SENSITIVE_CONFIG: string[] = [
+	'goroot', 'gopath', 'toolsGopath', 'alternateTools'
+];
+
+// Go extension configuration for a workspace.
+export class Configuration {
+	constructor(
+		private isTrustedWorkspace: boolean,
+		private getConfiguration: typeof vscode.workspace.getConfiguration) { }
+
+	// returns a Proxied vscode.WorkspaceConfiguration, which prevents
+	// from using the workspace configuration if the workspace is untrusted.
+	public get<T>(uri?: vscode.Uri): vscode.WorkspaceConfiguration {
+		const cfg = this.getConfiguration('go', uri);
+		if (this.isTrustedWorkspace) {
+			return cfg;
+		}
+
+		return new WrappedConfiguration(cfg);
+	}
+}
+
+// wrappedConfiguration wraps vscode.WorkspaceConfiguration.
+class WrappedConfiguration implements vscode.WorkspaceConfiguration {
+	constructor(private readonly _wrapped: vscode.WorkspaceConfiguration) {
+		// set getters for direct setting access (e.g. cfg.gopath), but don't overwrite _wrapped.
+		const desc = Object.getOwnPropertyDescriptors(_wrapped);
+		for (const prop in desc) {
+			if (typeof prop === 'string' && prop !== '_wrapped') {
+				const d = desc[prop];
+				if (SECURITY_SENSITIVE_CONFIG.includes(prop)) {
+					const inspect = this._wrapped.inspect(prop);
+					d.value = inspect.globalValue ?? inspect.defaultValue;
+				}
+				Object.defineProperty(this, prop, desc[prop]);
+			}
+		}
+	}
+
+	public get(section: any, defaultValue?: any) {
+		if (SECURITY_SENSITIVE_CONFIG.includes(section)) {
+			const inspect = this._wrapped.inspect(section);
+			return inspect.globalValue ?? defaultValue ?? inspect.defaultValue;
+		}
+		return this._wrapped.get(section, defaultValue);
+	}
+	public has(section: string) {
+		return this._wrapped.has(section);
+	}
+	public inspect<T>(section: string) {
+		return this._wrapped.inspect<T>(section);
+	}
+	public update(
+		section: string, value: any, configurationTarget?: boolean | vscode.ConfigurationTarget,
+		overrideInLanguage?: boolean): Thenable<void> {
+		return this._wrapped.update(section, value, configurationTarget, overrideInLanguage);
+	}
+}
diff --git a/test/integration/config.test.ts b/test/integration/config.test.ts
new file mode 100644
index 0000000..4569d9f
--- /dev/null
+++ b/test/integration/config.test.ts
@@ -0,0 +1,86 @@
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+'use strict';
+
+import * as assert from 'assert';
+import vscode = require('vscode');
+import { Configuration } from '../../src/config';
+
+suite('GoConfiguration Tests', () => {
+	function check(trusted: boolean, workspaceConfig: { [key: string]: any }, key: string, expected: any) {
+		const getConfigurationFn = (section: string) => new MockCfg(workspaceConfig);
+		const cfg = (new Configuration(trusted, getConfigurationFn)).get();
+
+		const got0 = JSON.stringify(cfg.get(key));
+		const got1 = JSON.stringify(cfg[key]);
+		const want = JSON.stringify(expected);
+		assert.strictEqual(got0, want, `cfg.get(${key}) = ${got0}, want ${want}`);
+		assert.strictEqual(got1, want, `cfg[${key}] = ${got1}, want ${want}`);
+
+	}
+	test('trusted workspace', () => {
+		check(true, { goroot: 'goroot_val' }, 'goroot', 'goroot_val');
+		check(true, { gopath: 'gopath_val' }, 'gopath', 'gopath_val');
+		check(true, { toolsGopath: 'toolsGopath_val' }, 'toolsGopath', 'toolsGopath_val');
+		check(true, { alternateTools: { go: 'foo' } }, 'alternateTools', { go: 'foo' });
+
+		check(true, { buildFlags: ['-v'] }, 'buildFlags', ['-v']);
+		check(true, { languageServerFlags: ['-rpc.trace'] }, 'languageServerFlags', ['-rpc.trace']);
+	});
+
+	test('untrusted workspace', () => {
+		check(false, { goroot: 'goroot_val' }, 'goroot', null);
+		check(false, { gopath: 'gopath_val' }, 'gopath', null);
+		check(false, { toolsGopath: 'toolsGopath_val' }, 'toolsGopath', null);
+		check(false, { alternateTools: { go: 'foo' } }, 'alternateTools', {});
+
+		check(false, { buildFlags: ['-v'] }, 'buildFlags', ['-v']);
+		check(false, { languageServerFlags: ['-rpc.trace'] }, 'languageServerFlags', ['-rpc.trace']);
+	});
+});
+
+// tslint:disable: no-any
+class MockCfg implements vscode.WorkspaceConfiguration {
+	private map: Map<string, any>;
+	private wrapped: vscode.WorkspaceConfiguration;
+
+	constructor(workspaceSettings: { [key: string]: any } = {}) {
+		// getter
+		Object.defineProperties(this, Object.getOwnPropertyDescriptors(workspaceSettings));
+		this.map = new Map<string, any>(Object.entries(workspaceSettings));
+		this.wrapped = vscode.workspace.getConfiguration('go');
+	}
+
+	// tslint:disable: no-any
+	public get(section: string, defaultValue?: any): any {
+		if (this.map.has(section)) {
+			return this.map.get(section);
+		}
+		return this.wrapped.get(section, defaultValue);
+	}
+
+	public has(section: string): boolean {
+		if (this.map.has(section)) {
+			return true;
+		}
+		return this.wrapped.has(section);
+	}
+
+	public inspect<T>(section: string) {
+		const i = this.wrapped.inspect<T>(section);
+		if (this.map.has(section)) {
+			i.workspaceValue = this.map.get(section);
+		}
+		return i;
+	}
+
+	public update(
+		section: string, value: any,
+		configurationTarget?: boolean | vscode.ConfigurationTarget,
+		overrideInLanguage?: boolean): Thenable<void> {
+		throw new Error('Method not implemented.');
+	}
+}