diff --git a/.eslintrc.json b/.eslintrc.json index 22157840d84..da169cdcc0c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -248,6 +248,7 @@ "url", "util", "v8-inspect-profiler", + "vscode-policy-watcher", "vscode-proxy-agent", "vscode-regexpp", "vscode-textmate", diff --git a/build/.moduleignore b/build/.moduleignore index 22c1b4fe48f..a2e1b715e1b 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -107,6 +107,14 @@ vscode-encrypt/binding.gyp vscode-encrypt/README.md !vscode-encrypt/build/Release/vscode-encrypt-native.node +vscode-policy-watcher/build/** +vscode-policy-watcher/.husky/** +vscode-policy-watcher/src/** +vscode-policy-watcher/binding.gyp +vscode-policy-watcher/README.md +vscode-policy-watcher/index.d.ts +!vscode-policy-watcher/build/Release/vscode-policy-watcher.node + vscode-windows-ca-certs/**/* !vscode-windows-ca-certs/package.json !vscode-windows-ca-certs/**/*.node diff --git a/package.json b/package.json index b8992138afc..ad35220a1e4 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "tas-client-umd": "0.1.5", "v8-inspect-profiler": "^0.1.0", "vscode-oniguruma": "1.6.1", + "vscode-policy-watcher": "^1.1.0", "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 4dd6d42da9b..243729c4e37 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -100,6 +100,8 @@ import { InspectProfilingService as V8InspectProfilingService } from 'vs/platfor import { IV8InspectProfilingService } from 'vs/platform/profiling/common/profiling'; import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; +import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; class SharedProcessMain extends Disposable { @@ -180,6 +182,10 @@ class SharedProcessMain extends Disposable { const mainProcessService = new MessagePortMainProcessService(this.server, mainRouter); services.set(IMainProcessService, mainProcessService); + // Policies + const policyService = this.configuration.policiesData ? new PolicyChannelClient(this.configuration.policiesData, mainProcessService.getChannel('policy')) : new NullPolicyService(); + services.set(IPolicyService, policyService); + // Environment const environmentService = new SharedProcessEnvironmentService(this.configuration.args, productService); services.set(INativeEnvironmentService, environmentService); @@ -222,7 +228,7 @@ class SharedProcessMain extends Disposable { fileService.registerProvider(Schemas.vscodeUserData, userDataFileSystemProvider); // Configuration - const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService, policyService, logService)); services.set(IConfigurationService, configurationService); // Storage (global access only) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 651c9ea09e2..2d7e71bef34 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -99,6 +99,8 @@ import { IWorkspacesHistoryMainService, WorkspacesHistoryMainService } from 'vs/ import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { IWorkspacesManagementMainService, WorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { CredentialsNativeMainService } from 'vs/platform/credentials/electron-main/credentialsMainService'; +import { IPolicyService } from 'vs/platform/policy/common/policy'; +import { PolicyChannel } from 'vs/platform/policy/common/policyIpc'; /** * The main VS Code application. There will only ever be one instance, @@ -695,6 +697,11 @@ export class CodeApplication extends Disposable { const diagnosticsChannel = ProxyChannel.fromService(accessor.get(IDiagnosticsMainService), { disableMarshalling: true }); this.mainProcessNodeIpcServer.registerChannel('diagnostics', diagnosticsChannel); + // Policies (main & shared process) + const policyChannel = new PolicyChannel(accessor.get(IPolicyService)); + mainProcessElectronServer.registerChannel('policy', policyChannel); + sharedProcessClient.then(client => client.registerChannel('policy', policyChannel)); + // Local Files const diskFileSystemProvider = this.fileService.getProvider(Schemas.file); assertType(diskFileSystemProvider instanceof DiskFileSystemProvider); @@ -1141,7 +1148,7 @@ export class CodeApplication extends Disposable { // Initialize update service const updateService = accessor.get(IUpdateService); if (updateService instanceof Win32UpdateService || updateService instanceof LinuxUpdateService || updateService instanceof DarwinUpdateService) { - await updateService.initialize(); + updateService.initialize(); } // Start to fetch shell environment (if needed) after window has opened diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 762f288daba..cfa944e76c8 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -62,6 +62,10 @@ import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { StateMainService } from 'vs/platform/state/electron-main/stateMainService'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; +import { NativePolicyService } from 'vs/platform/policy/node/nativePolicyService'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { DisposableStore } from 'vs/base/common/lifecycle'; /** * The main VS Code entry point. @@ -89,13 +93,13 @@ class CodeMain { setUnexpectedErrorHandler(err => console.error(err)); // Create services - const [instantiationService, instanceEnvironment, environmentMainService, configurationService, stateMainService, bufferLogService, productService] = this.createServices(); + const [instantiationService, instanceEnvironment, environmentMainService, policyService, configurationService, stateMainService, bufferLogService, productService] = this.createServices(); try { // Init services try { - await this.initServices(environmentMainService, configurationService, stateMainService); + await this.initServices(environmentMainService, policyService, configurationService, stateMainService); } catch (error) { // Show a dialog for errors that can be resolved by the user @@ -137,8 +141,10 @@ class CodeMain { } } - private createServices(): [IInstantiationService, IProcessEnvironment, IEnvironmentMainService, ConfigurationService, StateMainService, BufferLogService, IProductService] { + private createServices(): [IInstantiationService, IProcessEnvironment, IEnvironmentMainService, IPolicyService, ConfigurationService, StateMainService, BufferLogService, IProductService] { const services = new ServiceCollection(); + const disposables = new DisposableStore(); + process.once('exit', () => disposables.dispose()); // Product const productService = { _serviceBrand: undefined, ...product }; @@ -153,8 +159,7 @@ class CodeMain { // we are the only instance running, otherwise we'll have concurrent // log file access on Windows (https://github.com/microsoft/vscode/issues/41218) const bufferLogService = new BufferLogService(); - const logService = new MultiplexLogService([new ConsoleMainLogger(getLogLevel(environmentMainService)), bufferLogService]); - process.once('exit', () => logService.dispose()); + const logService = disposables.add(new MultiplexLogService([new ConsoleMainLogger(getLogLevel(environmentMainService)), bufferLogService])); services.set(ILogService, logService); // Files @@ -166,8 +171,14 @@ class CodeMain { // Logger services.set(ILoggerService, new LoggerService(logService, fileService)); + // Policy + const policyService = isWindows && productService.win32RegValueName ? disposables.add(new NativePolicyService(productService.win32RegValueName)) + : environmentMainService.policyFile ? disposables.add(new FilePolicyService(environmentMainService.policyFile, fileService, logService)) + : new NullPolicyService(); + services.set(IPolicyService, policyService); + // Configuration - const configurationService = new ConfigurationService(environmentMainService.settingsResource, fileService); + const configurationService = new ConfigurationService(environmentMainService.settingsResource, fileService, policyService, logService); services.set(IConfigurationService, configurationService); // Lifecycle @@ -192,7 +203,7 @@ class CodeMain { // Protocol services.set(IProtocolMainService, new SyncDescriptor(ProtocolMainService)); - return [new InstantiationService(services, true), instanceEnvironment, environmentMainService, configurationService, stateMainService, bufferLogService, productService]; + return [new InstantiationService(services, true), instanceEnvironment, environmentMainService, policyService, configurationService, stateMainService, bufferLogService, productService]; } private patchEnvironment(environmentMainService: IEnvironmentMainService): IProcessEnvironment { @@ -212,7 +223,7 @@ class CodeMain { return instanceEnvironment; } - private initServices(environmentMainService: IEnvironmentMainService, configurationService: ConfigurationService, stateMainService: StateMainService): Promise { + private initServices(environmentMainService: IEnvironmentMainService, policyService: IPolicyService, configurationService: ConfigurationService, stateMainService: StateMainService): Promise { return Promises.settled([ // Environment service (paths) diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 9495713716e..6c197b17661 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -11,6 +11,7 @@ import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/err import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isAbsolute, join } from 'vs/base/common/path'; +import { isWindows } from 'vs/base/common/platform'; import { cwd } from 'vs/base/common/process'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -39,6 +40,9 @@ import { ILocalizationsService } from 'vs/platform/localizations/common/localiza import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { ConsoleLogger, getLogLevel, ILogger, ILogService, LogLevel, MultiplexLogService } from 'vs/platform/log/common/log'; import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; +import { NativePolicyService } from 'vs/platform/policy/node/nativePolicyService'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; import { IRequestService } from 'vs/platform/request/common/request'; @@ -128,8 +132,14 @@ class CliMain extends Disposable { const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); fileService.registerProvider(Schemas.file, diskFileSystemProvider); + // Policy + const policyService = isWindows && productService.win32RegValueName ? this._register(new NativePolicyService(productService.win32RegValueName)) + : environmentService.policyFile ? this._register(new FilePolicyService(environmentService.policyFile, fileService, logService)) + : new NullPolicyService(); + services.set(IPolicyService, policyService); + // Configuration - const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService, policyService, logService)); services.set(IConfigurationService, configurationService); // Init config diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index ef4d4407aa9..52803c25a6a 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -29,7 +29,7 @@ import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService import { ITextResourceConfigurationService, ITextResourcePropertiesService, ITextResourceConfigurationChangeEvent } from 'vs/editor/common/services/textResourceConfiguration'; import { CommandsRegistry, ICommandEvent, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationService, IConfigurationModel, IConfigurationValue, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { Configuration, ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; +import { Configuration, ConfigurationModel, ConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; import { IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IInputResult, IShowResult } from 'vs/platform/dialogs/common/dialogs'; import { createDecorator, IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -88,6 +88,7 @@ import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/com import { staticObservableValue } from 'vs/base/common/observableValue'; import 'vs/editor/common/services/languageFeaturesService'; +import { DefaultConfigurationModel } from 'vs/platform/configuration/common/configurations'; class SimpleModel implements IResolvedTextEditorModel { @@ -521,7 +522,7 @@ export class StandaloneConfigurationService implements IConfigurationService { private readonly _configuration: Configuration; constructor() { - this._configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel()); + this._configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); } getValue(): T; @@ -582,6 +583,7 @@ export class StandaloneConfigurationService implements IConfigurationService { }; return { defaults: emptyModel, + policy: emptyModel, user: emptyModel, workspace: emptyModel, folders: [] diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 6ed2b0c3ef8..4012d405858 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -80,6 +80,7 @@ export interface IConfigurationValue { readonly workspaceValue?: T; readonly workspaceFolderValue?: T; readonly memoryValue?: T; + readonly policyValue?: T; readonly value?: T; readonly default?: { value?: T; override?: T }; @@ -89,6 +90,7 @@ export interface IConfigurationValue { readonly workspace?: { value?: T; override?: T }; readonly workspaceFolder?: { value?: T; override?: T }; readonly memory?: { value?: T; override?: T }; + readonly policy?: { value?: T }; readonly overrideIdentifiers?: string[]; } @@ -163,6 +165,7 @@ export interface IOverrides { export interface IConfigurationData { defaults: IConfigurationModel; + policy: IConfigurationModel; user: IConfigurationModel; workspace: IConfigurationModel; folders: [UriComponents, IConfigurationModel][]; diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 8afa3b7f065..ad704762f68 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -21,7 +21,7 @@ import { Workspace } from 'vs/platform/workspace/common/workspace'; export class ConfigurationModel implements IConfigurationModel { - private isFrozen: boolean = false; + private frozen: boolean = false; private readonly overrideConfigurations = new Map(); constructor( @@ -47,6 +47,10 @@ export class ConfigurationModel implements IConfigurationModel { return this._keys.length === 0 && Object.keys(this._contents).length === 0 && this._overrides.length === 0; } + isFrozen(): boolean { + return this.frozen; + } + getValue(section: string | undefined): V { return section ? getConfigurationValue(this.contents, section) : this.contents; } @@ -113,10 +117,14 @@ export class ConfigurationModel implements IConfigurationModel { } freeze(): ConfigurationModel { - this.isFrozen = true; + this.frozen = true; return this; } + clone(): ConfigurationModel { + return new ConfigurationModel(objects.deepClone(this.contents), [...this.keys], objects.deepClone(this.overrides)); + } + private createOverrideConfigurationModel(identifier: string): ConfigurationModel { const overrideContents = this.getContentsForOverrideIdentifer(identifier); @@ -161,7 +169,7 @@ export class ConfigurationModel implements IConfigurationModel { } private checkAndFreeze(data: T): T { - if (this.isFrozen && !Object.isFrozen(data)) { + if (this.frozen && !Object.isFrozen(data)) { return objects.deepFreeze(data); } return data; @@ -232,33 +240,6 @@ export class ConfigurationModel implements IConfigurationModel { } } -export class DefaultConfigurationModel extends ConfigurationModel { - - constructor(configurationDefaultsOverrides: IStringDictionary = {}) { - const properties = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const keys = Object.keys(properties); - const contents: any = Object.create(null); - const overrides: IOverrides[] = []; - - for (const key in properties) { - const defaultOverrideValue = configurationDefaultsOverrides[key]; - const value = defaultOverrideValue !== undefined ? defaultOverrideValue : properties[key].default; - addToValueTree(contents, key, value, message => console.error(`Conflict in default settings: ${message}`)); - } - for (const key of Object.keys(contents)) { - if (OVERRIDE_PROPERTY_REGEX.test(key)) { - overrides.push({ - identifiers: overrideIdentifiersFromKey(key), - keys: Object.keys(contents[key]), - contents: toValuesTree(contents[key], message => console.error(`Conflict in default settings file: ${message}`)), - }); - } - } - - super(contents, keys, overrides); - } -} - export interface ConfigurationParseOptions { scopes: ConfigurationScope[] | undefined; skipRestricted?: boolean; @@ -473,6 +454,7 @@ export class Configuration { constructor( private _defaultConfiguration: ConfigurationModel, + private _policyConfiguration: ConfigurationModel, private _localUserConfiguration: ConfigurationModel, private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(), private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), @@ -483,7 +465,7 @@ export class Configuration { } getValue(section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined): any { - const consolidateConfigurationModel = this.getConsolidateConfigurationModel(overrides, workspace); + const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(section, overrides, workspace); return consolidateConfigurationModel.getValue(section); } @@ -511,11 +493,12 @@ export class Configuration { } inspect(key: string, overrides: IConfigurationOverrides, workspace: Workspace | undefined): IConfigurationValue { - const consolidateConfigurationModel = this.getConsolidateConfigurationModel(overrides, workspace); + const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(key, overrides, workspace); const folderConfigurationModel = this.getFolderConfigurationModelForResource(overrides.resource, workspace); const memoryConfigurationModel = overrides.resource ? this._memoryConfigurationByResource.get(overrides.resource) || this._memoryConfiguration : this._memoryConfiguration; const defaultValue = overrides.overrideIdentifier ? this._defaultConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._defaultConfiguration.freeze().getValue(key); + const policyValue = this._policyConfiguration.isEmpty() ? undefined : this._policyConfiguration.freeze().getValue(key); const userValue = overrides.overrideIdentifier ? this.userConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.userConfiguration.freeze().getValue(key); const userLocalValue = overrides.overrideIdentifier ? this.localUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.localUserConfiguration.freeze().getValue(key); const userRemoteValue = overrides.overrideIdentifier ? this.remoteUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.remoteUserConfiguration.freeze().getValue(key); @@ -523,19 +506,21 @@ export class Configuration { const workspaceFolderValue = folderConfigurationModel ? overrides.overrideIdentifier ? folderConfigurationModel.freeze().override(overrides.overrideIdentifier).getValue(key) : folderConfigurationModel.freeze().getValue(key) : undefined; const memoryValue = overrides.overrideIdentifier ? memoryConfigurationModel.override(overrides.overrideIdentifier).getValue(key) : memoryConfigurationModel.getValue(key); const value = consolidateConfigurationModel.getValue(key); - const overrideIdentifiers: string[] = arrays.distinct(arrays.flatten(consolidateConfigurationModel.overrides.map(override => override.identifiers))).filter(overrideIdentifier => consolidateConfigurationModel.getOverrideValue(key, overrideIdentifier) !== undefined); + const overrideIdentifiers: string[] = arrays.distinct(consolidateConfigurationModel.overrides.map(override => override.identifiers).flat()).filter(overrideIdentifier => consolidateConfigurationModel.getOverrideValue(key, overrideIdentifier) !== undefined); return { - defaultValue: defaultValue, - userValue: userValue, - userLocalValue: userLocalValue, - userRemoteValue: userRemoteValue, - workspaceValue: workspaceValue, - workspaceFolderValue: workspaceFolderValue, - memoryValue: memoryValue, + defaultValue, + policyValue, + userValue, + userLocalValue, + userRemoteValue, + workspaceValue, + workspaceFolderValue, + memoryValue, value, default: defaultValue !== undefined ? { value: this._defaultConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._defaultConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, + policy: policyValue !== undefined ? { value: policyValue } : undefined, user: userValue !== undefined ? { value: this.userConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.userConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, userLocal: userLocalValue !== undefined ? { value: this.localUserConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.localUserConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, userRemote: userRemoteValue !== undefined ? { value: this.remoteUserConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.remoteUserConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, @@ -568,6 +553,10 @@ export class Configuration { this._foldersConsolidatedConfigurations.clear(); } + updatePolicyConfiguration(policyConfiguration: ConfigurationModel): void { + this._policyConfiguration = policyConfiguration; + } + updateLocalUserConfiguration(localUserConfiguration: ConfigurationModel): void { this._localUserConfiguration = localUserConfiguration; this._userConfiguration = null; @@ -620,6 +609,15 @@ export class Configuration { return { keys, overrides }; } + compareAndUpdatePolicyConfiguration(policyConfiguration: ConfigurationModel): IConfigurationChange { + const { added, updated, removed } = compare(this._policyConfiguration, policyConfiguration); + const keys = [...added, ...updated, ...removed]; + if (keys.length) { + this.updatePolicyConfiguration(policyConfiguration); + } + return { keys, overrides: [] }; + } + compareAndUpdateLocalUserConfiguration(user: ConfigurationModel): IConfigurationChange { const { added, updated, removed, overrides } = compare(this.localUserConfiguration, user); const keys = [...added, ...updated, ...removed]; @@ -698,9 +696,15 @@ export class Configuration { return this._folderConfigurations; } - private getConsolidateConfigurationModel(overrides: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel { + private getConsolidatedConfigurationModel(section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel { let configurationModel = this.getConsolidatedConfigurationModelForResource(overrides, workspace); - return overrides.overrideIdentifier ? configurationModel.override(overrides.overrideIdentifier) : configurationModel; + if (overrides.overrideIdentifier) { + configurationModel = configurationModel.override(overrides.overrideIdentifier); + } + if (!this._policyConfiguration.isEmpty() && this._policyConfiguration.getValue(section) !== undefined) { + configurationModel = configurationModel.merge(this._policyConfiguration); + } + return configurationModel; } private getConsolidatedConfigurationModelForResource({ resource }: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel { @@ -765,6 +769,11 @@ export class Configuration { overrides: this._defaultConfiguration.overrides, keys: this._defaultConfiguration.keys }, + policy: { + contents: this._policyConfiguration.contents, + overrides: this._policyConfiguration.overrides, + keys: this._policyConfiguration.keys + }, user: { contents: this.userConfiguration.contents, overrides: this.userConfiguration.overrides, @@ -812,13 +821,14 @@ export class Configuration { static parse(data: IConfigurationData): Configuration { const defaultConfiguration = this.parseConfigurationModel(data.defaults); + const policyConfiguration = this.parseConfigurationModel(data.policy); const userConfiguration = this.parseConfigurationModel(data.user); const workspaceConfiguration = this.parseConfigurationModel(data.workspace); const folders: ResourceMap = data.folders.reduce((result, value) => { result.set(URI.revive(value[0]), this.parseConfigurationModel(value[1])); return result; }, new ResourceMap()); - return new Configuration(defaultConfiguration, userConfiguration, new ConfigurationModel(), workspaceConfiguration, folders, new ConfigurationModel(), new ResourceMap(), false); + return new Configuration(defaultConfiguration, policyConfiguration, userConfiguration, new ConfigurationModel(), workspaceConfiguration, folders, new ConfigurationModel(), new ResourceMap(), false); } private static parseConfigurationModel(model: IConfigurationModel): ConfigurationModel { diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 4cf74ba2ff8..c4e17cfbfd9 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -11,6 +11,7 @@ import * as types from 'vs/base/common/types'; import * as nls from 'vs/nls'; import { getLanguageTagSettingPlainKey } from 'vs/platform/configuration/common/configuration'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { PolicyName } from 'vs/platform/policy/common/policy'; import { Registry } from 'vs/platform/registry/common/platform'; export enum EditPresentationTypes { @@ -89,6 +90,11 @@ export interface IConfigurationRegistry { */ getConfigurationProperties(): IStringDictionary; + /** + * Return all configurations by policy name + */ + getPolicyConfigurations(): Map; + /** * Returns all excluded configurations settings of all configuration nodes contributed to this registry. */ @@ -127,12 +133,12 @@ export const enum ConfigurationScope { MACHINE_OVERRIDABLE, } -export interface PolicyConfiguration { +export interface IPolicy { /** * The policy name. */ - readonly name: string; + readonly name: PolicyName; /** * The Code version in which this policy was introduced. @@ -193,7 +199,7 @@ export interface IConfigurationPropertySchema extends IJSONSchema { * When specified, this setting's value can always be overwritten by * a system-wide policy. */ - policy?: PolicyConfiguration; + policy?: IPolicy; } export interface IExtensionInfo { @@ -249,6 +255,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode; private readonly configurationContributors: IConfigurationNode[]; private readonly configurationProperties: IStringDictionary; + private readonly policyConfigurations: Map; private readonly excludedConfigurationProperties: IStringDictionary; private readonly resourceLanguageSettingsSchema: IJSONSchema; private readonly overrideIdentifiers = new Set(); @@ -269,6 +276,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { this.configurationContributors = [this.defaultLanguageConfigurationOverridesNode]; this.resourceLanguageSettingsSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown editor configuration setting', allowTrailingCommas: true, allowComments: true }; this.configurationProperties = {}; + this.policyConfigurations = new Map(); this.excludedConfigurationProperties = {}; contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema); @@ -409,6 +417,10 @@ class ConfigurationRegistry implements IConfigurationRegistry { if (configuration.properties) { for (const key in configuration.properties) { properties.push(key); + const property = this.configurationProperties[key]; + if (property?.policy?.name) { + this.policyConfigurations.delete(property.policy.name); + } delete this.configurationProperties[key]; this.removeFromSchema(key, configuration.properties[key]); } @@ -433,12 +445,12 @@ class ConfigurationRegistry implements IConfigurationRegistry { let properties = configuration.properties; if (properties) { for (let key in properties) { - if (validate && validateProperty(key)) { + const property: IRegisteredConfigurationPropertySchema = properties[key]; + if (validate && validateProperty(key, property)) { delete properties[key]; continue; } - const property: IRegisteredConfigurationPropertySchema = properties[key]; property.source = extensionInfo; // update default value @@ -461,6 +473,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { continue; } else { this.configurationProperties[key] = properties[key]; + if (properties[key].policy?.name) { + this.policyConfigurations.set(properties[key].policy!.name, key); + } } if (!properties[key].deprecationMessage && properties[key].markdownDeprecationMessage) { @@ -489,6 +504,10 @@ class ConfigurationRegistry implements IConfigurationRegistry { return this.configurationProperties; } + getPolicyConfigurations(): Map { + return this.policyConfigurations; + } + getExcludedConfigurationProperties(): IStringDictionary { return this.excludedConfigurationProperties; } @@ -659,7 +678,7 @@ export function getDefaultValue(type: string | string[] | undefined): any { const configurationRegistry = new ConfigurationRegistry(); Registry.add(Extensions.Configuration, configurationRegistry); -export function validateProperty(property: string): string | null { +export function validateProperty(property: string, schema: IRegisteredConfigurationPropertySchema): string | null { if (!property.trim()) { return nls.localize('config.property.empty', "Cannot register an empty property"); } @@ -669,6 +688,9 @@ export function validateProperty(property: string): string | null { if (configurationRegistry.getConfigurationProperties()[property] !== undefined) { return nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", property); } + if (schema.policy?.name && configurationRegistry.getPolicyConfigurations().get(schema.policy?.name) !== undefined) { + return nls.localize('config.policy.duplicate', "Cannot register '{0}'. The associated policy {1} is already registered with {2}.", property, schema.policy?.name, configurationRegistry.getPolicyConfigurations().get(schema.policy?.name)); + } return null; } diff --git a/src/vs/platform/configuration/common/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts index 7860c72ea8e..99da4050566 100644 --- a/src/vs/platform/configuration/common/configurationService.ts +++ b/src/vs/platform/configuration/common/configurationService.ts @@ -9,17 +9,20 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ConfigurationTarget, IConfigurationChange, IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationService, IConfigurationValue, isConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; -import { Configuration, ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel, UserSettings } from 'vs/platform/configuration/common/configurationModels'; -import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Configuration, ConfigurationChangeEvent, ConfigurationModel, UserSettings } from 'vs/platform/configuration/common/configurationModels'; +import { DefaultConfiguration, IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; import { IFileService } from 'vs/platform/files/common/files'; -import { Registry } from 'vs/platform/registry/common/platform'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable { declare readonly _serviceBrand: undefined; private configuration: Configuration; - private userConfiguration: UserSettings; + private readonly defaultConfiguration: DefaultConfiguration; + private readonly policyConfiguration: IPolicyConfiguration; + private readonly userConfiguration: UserSettings; private readonly reloadConfigurationScheduler: RunOnceScheduler; private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); @@ -27,20 +30,25 @@ export class ConfigurationService extends Disposable implements IConfigurationSe constructor( private readonly settingsResource: URI, - fileService: IFileService + fileService: IFileService, + policyService: IPolicyService, + logService: ILogService, ) { super(); + this.defaultConfiguration = this._register(new DefaultConfiguration()); + this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService)); this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, extUriBiasedIgnorePathCase, fileService)); - this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel()); + this.configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, new ConfigurationModel()); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50)); - this._register(Registry.as(Extensions.Configuration).onDidUpdateConfiguration(({ properties }) => this.onDidDefaultConfigurationChange(properties))); + this._register(this.defaultConfiguration.onDidChangeConfiguration(({ defaults, properties }) => this.onDidDefaultConfigurationChange(defaults, properties))); + this._register(this.policyConfiguration.onDidChangeConfiguration(model => this.onDidPolicyConfigurationChange(model))); this._register(this.userConfiguration.onDidChange(() => this.reloadConfigurationScheduler.schedule())); } async initialize(): Promise { - const userConfiguration = await this.userConfiguration.loadConfiguration(); - this.configuration = new Configuration(new DefaultConfigurationModel(), userConfiguration); + const [defaultModel, policyModel, userModel] = await Promise.all([this.defaultConfiguration.initialize(), this.policyConfiguration.initialize(), this.userConfiguration.loadConfiguration()]); + this.configuration = new Configuration(defaultModel, policyModel, userModel); } getConfigurationData(): IConfigurationData { @@ -89,9 +97,15 @@ export class ConfigurationService extends Disposable implements IConfigurationSe this.trigger(change, previous, ConfigurationTarget.USER); } - private onDidDefaultConfigurationChange(properties: string[]): void { + private onDidDefaultConfigurationChange(defaultConfigurationModel: ConfigurationModel, properties: string[]): void { const previous = this.configuration.toData(); - const change = this.configuration.compareAndUpdateDefaultConfiguration(new DefaultConfigurationModel(), properties); + const change = this.configuration.compareAndUpdateDefaultConfiguration(defaultConfigurationModel, properties); + this.trigger(change, previous, ConfigurationTarget.DEFAULT); + } + + private onDidPolicyConfigurationChange(policyConfiguration: ConfigurationModel): void { + const previous = this.configuration.toData(); + const change = this.configuration.compareAndUpdatePolicyConfiguration(policyConfiguration); this.trigger(change, previous, ConfigurationTarget.DEFAULT); } diff --git a/src/vs/platform/configuration/common/configurations.ts b/src/vs/platform/configuration/common/configurations.ts new file mode 100644 index 00000000000..81133870a7a --- /dev/null +++ b/src/vs/platform/configuration/common/configurations.ts @@ -0,0 +1,190 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { coalesce } from 'vs/base/common/arrays'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { equals } from 'vs/base/common/objects'; +import { isEmptyObject } from 'vs/base/common/types'; +import { addToValueTree, IOverrides, toValuesTree } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; +import { Extensions, IConfigurationRegistry, overrideIdentifiersFromKey, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IPolicyService, PolicyDefinition, PolicyName, PolicyValue } from 'vs/platform/policy/common/policy'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export class DefaultConfiguration extends Disposable { + + private readonly _onDidChangeConfiguration = this._register(new Emitter<{ defaults: ConfigurationModel; properties: string[] }>()); + readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; + + private _configurationModel: ConfigurationModel | undefined; + get configurationModel(): ConfigurationModel { + if (!this._configurationModel) { + this._configurationModel = new DefaultConfigurationModel(this.getConfigurationDefaultOverrides()); + } + return this._configurationModel; + } + + async initialize(): Promise { + this._configurationModel = undefined; + this._register(Registry.as(Extensions.Configuration).onDidUpdateConfiguration(({ properties, defaultsOverrides }) => this.onDidUpdateConfiguration(properties, defaultsOverrides))); + return this.configurationModel; + } + + reload(): ConfigurationModel { + this._configurationModel = undefined; + return this.configurationModel; + } + + protected onDidUpdateConfiguration(properties: string[], defaultsOverrides?: boolean): void { + this._configurationModel = undefined; + this._onDidChangeConfiguration.fire({ defaults: this.configurationModel, properties }); + } + + protected getConfigurationDefaultOverrides(): IStringDictionary { + return {}; + } + +} + +export class DefaultConfigurationModel extends ConfigurationModel { + + constructor(configurationDefaultsOverrides: IStringDictionary = {}) { + const properties = Registry.as(Extensions.Configuration).getConfigurationProperties(); + const keys = Object.keys(properties); + const contents: any = Object.create(null); + const overrides: IOverrides[] = []; + + for (const key in properties) { + const defaultOverrideValue = configurationDefaultsOverrides[key]; + const value = defaultOverrideValue !== undefined ? defaultOverrideValue : properties[key].default; + addToValueTree(contents, key, value, message => console.error(`Conflict in default settings: ${message}`)); + } + for (const key of Object.keys(contents)) { + if (OVERRIDE_PROPERTY_REGEX.test(key)) { + overrides.push({ + identifiers: overrideIdentifiersFromKey(key), + keys: Object.keys(contents[key]), + contents: toValuesTree(contents[key], message => console.error(`Conflict in default settings file: ${message}`)), + }); + } + } + + super(contents, keys, overrides); + } +} + +export interface IPolicyConfiguration { + readonly onDidChangeConfiguration: Event; + readonly configurationModel: ConfigurationModel; + initialize(): Promise; +} + +export class NullPolicyConfiguration implements IPolicyConfiguration { + readonly onDidChangeConfiguration = Event.None; + readonly configurationModel = new ConfigurationModel(); + async initialize() { return this.configurationModel; } +} + +export class PolicyConfiguration extends Disposable implements IPolicyConfiguration { + + private readonly _onDidChangeConfiguration = this._register(new Emitter()); + readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; + + private _configurationModel = new ConfigurationModel(); + get configurationModel() { return this._configurationModel; } + + constructor( + private readonly defaultConfiguration: DefaultConfiguration, + @IPolicyService private readonly policyService: IPolicyService, + @ILogService private readonly logService: ILogService + ) { + super(); + } + + async initialize(): Promise { + this.update(await this.registerPolicyDefinitions(this.defaultConfiguration.configurationModel.keys), false); + this._register(this.policyService.onDidChange(policyNames => this.onDidChangePolicies(policyNames))); + this._register(this.defaultConfiguration.onDidChangeConfiguration(async ({ properties }) => this.update(await this.registerPolicyDefinitions(properties), true))); + return this._configurationModel; + } + + private async registerPolicyDefinitions(properties: string[]): Promise { + const policyDefinitions: IStringDictionary = {}; + const keys: string[] = []; + const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); + + for (const key of properties) { + const config = configurationProperties[key]; + if (!config) { + // Config is removed. So add it to the list if in case it was registered as policy before + keys.push(key); + continue; + } + if (config.policy) { + if (config.type !== 'string' && config.type !== 'number') { + this.logService.warn(`Policy ${config.policy.name} has unsupported type ${config.type}`); + continue; + } + keys.push(key); + policyDefinitions[config.policy.name] = { type: config.type }; + } + } + + if (!isEmptyObject(policyDefinitions)) { + await this.policyService.registerPolicyDefinitions(policyDefinitions); + } + + return keys; + } + + private onDidChangePolicies(policyNames: readonly PolicyName[]): void { + const policyConfigurations = Registry.as(Extensions.Configuration).getPolicyConfigurations(); + const keys = coalesce(policyNames.map(policyName => policyConfigurations.get(policyName))); + this.update(keys, true); + } + + private update(keys: string[], trigger: boolean): void { + const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); + const changed: [string, PolicyValue | undefined][] = []; + const wasEmpty = this._configurationModel.isEmpty(); + + for (const key of keys) { + const policyName = configurationProperties[key]?.policy?.name; + if (policyName) { + const policyValue = this.policyService.getPolicyValue(policyName); + if (wasEmpty ? policyValue !== undefined : !equals(this._configurationModel.getValue(key), policyValue)) { + changed.push([key, policyValue]); + } + } else { + if (this._configurationModel.getValue(key) !== undefined) { + changed.push([key, undefined]); + } + } + } + + if (changed.length) { + const old = this._configurationModel; + this._configurationModel = new ConfigurationModel(); + for (const key of old.keys) { + this._configurationModel.setValue(key, old.getValue(key)); + } + for (const [key, policyValue] of changed) { + if (policyValue === undefined) { + this._configurationModel.removeValue(key); + } else { + this._configurationModel.setValue(key, policyValue); + } + } + if (trigger) { + this._onDidChangeConfiguration.fire(this._configurationModel); + } + } + } + + +} diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index ae1add24a78..859cd0c28a1 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -6,8 +6,9 @@ import * as assert from 'assert'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { AllKeysConfigurationChangeEvent, Configuration, ConfigurationChangeEvent, ConfigurationModel, ConfigurationModelParser, DefaultConfigurationModel, mergeChanges } from 'vs/platform/configuration/common/configurationModels'; +import { AllKeysConfigurationChangeEvent, Configuration, ConfigurationChangeEvent, ConfigurationModel, ConfigurationModelParser, mergeChanges } from 'vs/platform/configuration/common/configurationModels'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { DefaultConfigurationModel } from 'vs/platform/configuration/common/configurations'; import { Registry } from 'vs/platform/registry/common/platform'; import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; @@ -486,7 +487,7 @@ suite('Configuration', () => { const defaultConfigurationModel = parseConfigurationModel({ '[l1]': { 'a': 1 }, '[l2]': { 'b': 1 } }); const userConfigurationModel = parseConfigurationModel({ '[l3]': { 'a': 2 } }); const workspaceConfigurationModel = parseConfigurationModel({ '[l1]': { 'a': 3 }, '[l4]': { 'a': 3 } }); - const testObject: Configuration = new Configuration(defaultConfigurationModel, userConfigurationModel, new ConfigurationModel(), workspaceConfigurationModel); + const testObject: Configuration = new Configuration(defaultConfigurationModel, new ConfigurationModel(), userConfigurationModel, workspaceConfigurationModel); const { overrideIdentifiers } = testObject.inspect('a', {}, undefined); @@ -496,7 +497,7 @@ suite('Configuration', () => { test('Test update value', () => { const parser = new ConfigurationModelParser('test'); parser.parse(JSON.stringify({ 'a': 1 })); - const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel()); + const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel(), new ConfigurationModel()); testObject.updateValue('a', 2); @@ -506,7 +507,7 @@ suite('Configuration', () => { test('Test update value after inspect', () => { const parser = new ConfigurationModelParser('test'); parser.parse(JSON.stringify({ 'a': 1 })); - const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel()); + const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel(), new ConfigurationModel()); testObject.inspect('a', {}, undefined); testObject.updateValue('a', 2); @@ -515,7 +516,7 @@ suite('Configuration', () => { }); test('Test compare and update default configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); testObject.updateDefaultConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on', })); @@ -532,7 +533,7 @@ suite('Configuration', () => { }); test('Test compare and update user configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); testObject.updateLocalUserConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, @@ -555,7 +556,7 @@ suite('Configuration', () => { }); test('Test compare and update workspace configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); testObject.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, @@ -578,7 +579,7 @@ suite('Configuration', () => { }); test('Test compare and update workspace folder configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); testObject.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, @@ -601,7 +602,7 @@ suite('Configuration', () => { }); test('Test compare and delete workspace folder configuration', () => { - const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); testObject.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'editor.lineNumbers': 'off', 'editor.fontSize': 12, @@ -627,7 +628,7 @@ suite('Configuration', () => { suite('ConfigurationChangeEvent', () => { test('changeEvent affecting keys with new configuration', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({ 'window.zoomLevel': 1, 'workbench.editor.enablePreview': false, @@ -653,7 +654,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting keys when configuration changed', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); configuration.updateLocalUserConfiguration(toConfigurationModel({ 'window.zoomLevel': 2, 'workbench.editor.enablePreview': true, @@ -682,7 +683,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting overrides with new configuration', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({ 'files.autoSave': 'off', '[markdown]': { @@ -724,7 +725,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting overrides when configuration changed', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); configuration.updateLocalUserConfiguration(toConfigurationModel({ 'workbench.editor.enablePreview': true, '[markdown]': { @@ -791,7 +792,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting workspace folders', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); configuration.updateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'custom' })); configuration.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true })); configuration.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true })); @@ -881,7 +882,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent - all', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); configuration.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true })); const data = configuration.toData(); const change = mergeChanges( @@ -976,7 +977,7 @@ suite('ConfigurationChangeEvent', () => { }); test('changeEvent affecting tasks and launches', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({ 'launch': { 'configuraiton': {} @@ -999,7 +1000,7 @@ suite('ConfigurationChangeEvent', () => { suite('AllKeysConfigurationChangeEvent', () => { test('changeEvent', () => { - const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel()); + const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel()); configuration.updateDefaultConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'off', '[markdown]': { diff --git a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts index 83314c83ad3..8e836674dae 100644 --- a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts +++ b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts @@ -50,4 +50,30 @@ suite('ConfigurationRegistry', () => { assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 }); }); + + test('registering multiple settings with same policy', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'policy1': { + 'type': 'object', + policy: { + name: 'policy', + minimumVersion: '1.0.0' + } + }, + 'policy2': { + 'type': 'object', + policy: { + name: 'policy', + minimumVersion: '1.0.0' + } + } + } + }); + const actual = configurationRegistry.getConfigurationProperties(); + assert.ok(actual['policy1'] !== undefined); + assert.ok(actual['policy2'] === undefined); + }); }); diff --git a/src/vs/platform/configuration/test/common/configurationService.test.ts b/src/vs/platform/configuration/test/common/configurationService.test.ts index 559803b0588..3b1155bf20a 100644 --- a/src/vs/platform/configuration/test/common/configurationService.test.ts +++ b/src/vs/platform/configuration/test/common/configurationService.test.ts @@ -16,6 +16,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { NullLogService } from 'vs/platform/log/common/log'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -36,7 +37,7 @@ suite('ConfigurationService', () => { test('simple', async () => { await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }')); - const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); await testObject.initialize(); const config = testObject.getValue<{ foo: string; @@ -49,7 +50,7 @@ suite('ConfigurationService', () => { test('config gets flattened', async () => { await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); - const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); await testObject.initialize(); const config = testObject.getValue<{ testworkbench: { @@ -68,7 +69,7 @@ suite('ConfigurationService', () => { test('error case does not explode', async () => { await fileService.writeFile(settingsResource, VSBuffer.fromString(',,,,')); - const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); await testObject.initialize(); const config = testObject.getValue<{ foo: string; @@ -78,7 +79,7 @@ suite('ConfigurationService', () => { }); test('missing file does not explode', async () => { - const testObject = disposables.add(new ConfigurationService(URI.file('__testFile'), fileService)); + const testObject = disposables.add(new ConfigurationService(URI.file('__testFile'), fileService, new NullPolicyService(), new NullLogService())); await testObject.initialize(); const config = testObject.getValue<{ foo: string }>(); @@ -87,7 +88,7 @@ suite('ConfigurationService', () => { }); test('trigger configuration change event when file does not exist', async () => { - const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); await testObject.initialize(); return new Promise((c, e) => { disposables.add(Event.filter(testObject.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(() => { @@ -100,7 +101,7 @@ suite('ConfigurationService', () => { }); test('trigger configuration change event when file exists', async () => { - const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }')); await testObject.initialize(); @@ -116,7 +117,7 @@ suite('ConfigurationService', () => { test('reloadConfiguration', async () => { await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }')); - const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); await testObject.initialize(); let config = testObject.getValue<{ foo: string; @@ -155,7 +156,7 @@ suite('ConfigurationService', () => { } }); - let testObject = disposables.add(new ConfigurationService(URI.file('__testFile'), fileService)); + let testObject = disposables.add(new ConfigurationService(URI.file('__testFile'), fileService, new NullPolicyService(), new NullLogService())); await testObject.initialize(); let setting = testObject.getValue(); @@ -163,7 +164,7 @@ suite('ConfigurationService', () => { assert.strictEqual(setting.configuration.service.testSetting, 'isSet'); await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); - testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); setting = testObject.getValue(); @@ -191,7 +192,7 @@ suite('ConfigurationService', () => { } }); - const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); testObject.initialize(); let res = testObject.inspect('something.missing'); @@ -226,7 +227,7 @@ suite('ConfigurationService', () => { } }); - const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); + const testObject = disposables.add(new ConfigurationService(settingsResource, fileService, new NullPolicyService(), new NullLogService())); testObject.initialize(); let res = testObject.inspect('lookup.service.testNullSetting'); diff --git a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts new file mode 100644 index 00000000000..d5b31e5516d --- /dev/null +++ b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { DefaultConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { deepClone } from 'vs/base/common/objects'; +import { IPolicyService } from 'vs/platform/policy/common/policy'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; + +suite('PolicyConfiguration', () => { + + let testObject: PolicyConfiguration; + let fileService: IFileService; + let policyService: IPolicyService; + const policyFile = URI.file('policyFile').with({ scheme: 'vscode-tests' }); + const disposables = new DisposableStore(); + const policyConfigurationNode: IConfigurationNode = { + 'id': 'policyConfiguration', + 'order': 1, + 'title': 'a', + 'type': 'object', + 'properties': { + 'policy.settingA': { + 'type': 'string', + 'default': 'defaultValueA', + policy: { + name: 'PolicySettingA', + minimumVersion: '1.0.0', + } + }, + 'policy.settingB': { + 'type': 'string', + 'default': 'defaultValueB', + policy: { + name: 'PolicySettingB', + minimumVersion: '1.0.0', + } + }, + 'nonPolicy.setting': { + 'type': 'boolean', + 'default': true + } + } + }; + + suiteSetup(() => Registry.as(Extensions.Configuration).registerConfiguration(policyConfigurationNode)); + suiteTeardown(() => Registry.as(Extensions.Configuration).deregisterConfigurations([policyConfigurationNode])); + + setup(async () => { + const defaultConfiguration = disposables.add(new DefaultConfiguration()); + await defaultConfiguration.initialize(); + fileService = disposables.add(new FileService(new NullLogService())); + const diskFileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(policyFile.scheme, diskFileSystemProvider); + policyService = new FilePolicyService(policyFile, fileService, new NullLogService()); + testObject = disposables.add(new PolicyConfiguration(defaultConfiguration, policyService, new NullLogService())); + }); + + teardown(() => disposables.clear()); + + test('initialize: with policies', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA' }))); + + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.strictEqual(acutal.getValue('policy.settingA'), 'policyValueA'); + assert.strictEqual(acutal.getValue('policy.settingB'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingA']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('initialize: no policies', async () => { + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.deepStrictEqual(acutal.keys, []); + assert.deepStrictEqual(acutal.overrides, []); + assert.strictEqual(acutal.getValue('policy.settingA'), undefined); + assert.strictEqual(acutal.getValue('policy.settingB'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + }); + + test('initialize: with policies but not registered', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA', 'PolicySettingB': 'policyValueB', 'PolicySettingC': 'policyValueC' }))); + + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.strictEqual(acutal.getValue('policy.settingA'), 'policyValueA'); + assert.strictEqual(acutal.getValue('policy.settingB'), 'policyValueB'); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingA', 'policy.settingB']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy is added', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA' }))); + await testObject.initialize(); + + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA', 'PolicySettingB': 'policyValueB', 'PolicySettingC': 'policyValueC' }))); + await promise; + }); + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingA'), 'policyValueA'); + assert.strictEqual(acutal.getValue('policy.settingB'), 'policyValueB'); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingA', 'policy.settingB']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy is updated', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA' }))); + await testObject.initialize(); + + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueAChanged' }))); + await promise; + }); + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingA'), 'policyValueAChanged'); + assert.strictEqual(acutal.getValue('policy.settingB'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingA']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy is removed', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA' }))); + await testObject.initialize(); + + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({}))); + await promise; + }); + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingA'), undefined); + assert.strictEqual(acutal.getValue('policy.settingB'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, []); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy setting is registered', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingC': 'policyValueC' }))); + await testObject.initialize(); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + policyConfigurationNode.properties!['policy.settingC'] = { + 'type': 'string', + 'default': 'defaultValueC', + policy: { + name: 'PolicySettingC', + minimumVersion: '1.0.0', + } + }; + Registry.as(Extensions.Configuration).registerConfiguration(deepClone(policyConfigurationNode)); + await promise; + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingC'), 'policyValueC'); + assert.strictEqual(acutal.getValue('policy.settingA'), undefined); + assert.strictEqual(acutal.getValue('policy.settingB'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingC']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy setting is deregistered', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA' }))); + await testObject.initialize(); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + Registry.as(Extensions.Configuration).deregisterConfigurations([policyConfigurationNode]); + await promise; + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingA'), undefined); + assert.strictEqual(acutal.getValue('policy.settingB'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, []); + assert.deepStrictEqual(acutal.overrides, []); + }); + +}); diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index b2b4500a614..1c903e2d25f 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -88,6 +88,7 @@ export interface NativeParsedArgs { 'sync'?: 'on' | 'off'; '__sandbox'?: boolean; 'logsPath'?: string; + '__enable-file-policy'?: boolean; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 56531a3a784..e0feccde448 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -87,6 +87,9 @@ export interface IEnvironmentService { telemetryLogResource: URI; serviceMachineIdResource: URI; + // --- Policy + policyFile?: URI; + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // // NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE. diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index fcea3f58023..1d7d75afbb9 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -237,6 +237,19 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron @memoize get disableWorkspaceTrust(): boolean { return !!this.args['disable-workspace-trust']; } + @memoize + get policyFile(): URI | undefined { + if (this.args['__enable-file-policy']) { + const vscodePortable = env['VSCODE_PORTABLE']; + if (vscodePortable) { + return URI.file(join(vscodePortable, 'policy.json')); + } + + return joinPath(this.userHome, this.productService.dataFolderName, 'policy.json'); + } + return undefined; + } + get args(): NativeParsedArgs { return this._args; } constructor( diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 062ce48a580..239aaaf7045 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -125,6 +125,7 @@ export const OPTIONS: OptionDescriptions> = { 'open-devtools': { type: 'boolean' }, '__sandbox': { type: 'boolean' }, 'logsPath': { type: 'string' }, + '__enable-file-policy': { type: 'boolean' }, // chromium flags 'no-proxy-server': { type: 'boolean' }, diff --git a/src/vs/platform/policy/common/filePolicyService.ts b/src/vs/platform/policy/common/filePolicyService.ts new file mode 100644 index 00000000000..71d2ed5fd87 --- /dev/null +++ b/src/vs/platform/policy/common/filePolicyService.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ThrottledDelayer } from 'vs/base/common/async'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; +import { isObject } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { AbstractPolicyService, IPolicyService, PolicyDefinition, PolicyName, PolicyValue } from 'vs/platform/policy/common/policy'; + +function keysDiff(a: Map, b: Map): string[] { + const result: string[] = []; + + for (const key of Iterable.concat(a.keys(), b.keys())) { + if (a.get(key) !== b.get(key)) { + result.push(key); + } + } + + return result; +} + +export class FilePolicyService extends AbstractPolicyService implements IPolicyService { + + private readonly throttledDelayer = this._register(new ThrottledDelayer(500)); + + constructor( + private readonly file: URI, + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService + ) { + super(); + + const onDidChangePolicyFile = Event.filter(fileService.onDidFilesChange, e => e.affects(file)); + this._register(fileService.watch(file)); + this._register(onDidChangePolicyFile(() => this.throttledDelayer.trigger(() => this.refresh()))); + } + + protected async initializePolicies(policyDefinitions: IStringDictionary): Promise { + await this.refresh(); + } + + private async read(): Promise> { + const policies = new Map(); + + try { + const content = await this.fileService.readFile(this.file); + const raw = JSON.parse(content.value.toString()); + + if (!isObject(raw)) { + throw new Error('Policy file isn\'t a JSON object'); + } + + for (const key of Object.keys(raw)) { + if (this.policyDefinitions[key]) { + policies.set(key, raw[key]); + } + } + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(`[FilePolicyService] Failed to read policies`, error); + } + } + + return policies; + } + + private async refresh(): Promise { + const policies = await this.read(); + const diff = keysDiff(this.policies, policies); + this.policies = policies; + + if (diff.length > 0) { + this._onDidChange.fire(diff); + } + } +} diff --git a/src/vs/platform/policy/common/policy.ts b/src/vs/platform/policy/common/policy.ts new file mode 100644 index 00000000000..d02ddfb77ef --- /dev/null +++ b/src/vs/platform/policy/common/policy.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IStringDictionary } from 'vs/base/common/collections'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export type PolicyName = string; +export type PolicyValue = string | number; +export type PolicyDefinition = { type: 'string' | 'number' }; + +export const IPolicyService = createDecorator('policy'); + +export interface IPolicyService { + readonly _serviceBrand: undefined; + + readonly onDidChange: Event; + registerPolicyDefinitions(policyDefinitions: IStringDictionary): Promise>; + getPolicyValue(name: PolicyName): PolicyValue | undefined; + serialize(): IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }> | undefined; +} + +export abstract class AbstractPolicyService extends Disposable implements IPolicyService { + readonly _serviceBrand: undefined; + + protected policyDefinitions: IStringDictionary = {}; + protected policies = new Map(); + + protected readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + async registerPolicyDefinitions(policyDefinitions: IStringDictionary): Promise> { + const size = Object.keys(this.policyDefinitions).length; + this.policyDefinitions = { ...policyDefinitions, ...this.policyDefinitions }; + + if (size !== Object.keys(this.policyDefinitions).length) { + await this.initializePolicies(policyDefinitions); + } + + return Iterable.reduce(this.policies.entries(), (r, [name, value]) => ({ ...r, [name]: value }), {}); + } + + getPolicyValue(name: PolicyName): PolicyValue | undefined { + return this.policies.get(name); + } + + serialize(): IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }> { + return Iterable.reduce<[PolicyName, PolicyDefinition], IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }>>(Object.entries(this.policyDefinitions), (r, [name, definition]) => ({ ...r, [name]: { definition, value: this.policies.get(name)! } }), {}); + } + + protected abstract initializePolicies(policyDefinitions: IStringDictionary): Promise; +} + +export class NullPolicyService implements IPolicyService { + readonly _serviceBrand: undefined; + readonly onDidChange = Event.None; + async registerPolicyDefinitions() { return {}; } + getPolicyValue() { return undefined; } + serialize() { return undefined; } +} diff --git a/src/vs/platform/policy/common/policyIpc.ts b/src/vs/platform/policy/common/policyIpc.ts new file mode 100644 index 00000000000..cdca636e999 --- /dev/null +++ b/src/vs/platform/policy/common/policyIpc.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IStringDictionary } from 'vs/base/common/collections'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { AbstractPolicyService, IPolicyService, PolicyDefinition, PolicyName, PolicyValue } from 'vs/platform/policy/common/policy'; + +export class PolicyChannel implements IServerChannel { + + private readonly disposables = new DisposableStore(); + + constructor(private service: IPolicyService) { } + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChange': return Event.map( + this.service.onDidChange, + names => names.reduce((r, name) => ({ ...r, [name]: this.service.getPolicyValue(name) ?? null }), {}), + this.disposables + ); + } + + throw new Error(`Event not found: ${event}`); + } + + call(_: unknown, command: string, arg?: any): Promise { + switch (command) { + case 'registerPolicyDefinitions': return this.service.registerPolicyDefinitions(arg as IStringDictionary); + } + + throw new Error(`Call not found: ${command}`); + } + + dispose() { + this.disposables.dispose(); + } +} + +export class PolicyChannelClient extends AbstractPolicyService implements IPolicyService { + + constructor(policiesData: IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }>, private readonly channel: IChannel) { + super(); + for (const name in policiesData) { + const { definition, value } = policiesData[name]; + this.policyDefinitions[name] = definition; + if (value !== undefined) { + this.policies.set(name, value); + } + } + this.channel.listen('onDidChange')(policies => { + for (const name in policies) { + const value = policies[name as keyof typeof policies]; + + if (value === null) { + this.policies.delete(name); + } else { + this.policies.set(name, value); + } + } + + this._onDidChange.fire(Object.keys(policies)); + }); + } + + protected async initializePolicies(policyDefinitions: IStringDictionary): Promise { + const result = await this.channel.call<{ [name: PolicyName]: PolicyValue }>('registerPolicyDefinitions', policyDefinitions); + for (const name in result) { + this.policies.set(name, result[name]); + } + } + +} diff --git a/src/vs/platform/policy/node/nativePolicyService.ts b/src/vs/platform/policy/node/nativePolicyService.ts new file mode 100644 index 00000000000..4d0e46d9d06 --- /dev/null +++ b/src/vs/platform/policy/node/nativePolicyService.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AbstractPolicyService, IPolicyService, PolicyDefinition } from 'vs/platform/policy/common/policy'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { Throttler } from 'vs/base/common/async'; +import { createWatcher, Watcher } from 'vscode-policy-watcher'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; + +export class NativePolicyService extends AbstractPolicyService implements IPolicyService { + + private throttler = new Throttler(); + private watcher = this._register(new MutableDisposable()); + + constructor(private readonly productName: string) { + super(); + } + + protected async initializePolicies(policyDefinitions: IStringDictionary): Promise { + await this.throttler.queue(() => new Promise((c, e) => { + try { + this.watcher.value = createWatcher(this.productName, policyDefinitions, update => { + for (const key in update) { + const value = update[key] as any; + + if (value === undefined) { + this.policies.delete(key); + } else { + this.policies.set(key, value); + } + } + + this._onDidChange.fire(Object.keys(update)); + c(); + }); + } catch (err) { + e(err); + } + })); + } + +} diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index f7046a4d756..0ccd16e61e5 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -22,6 +22,7 @@ import { ISharedProcessWorkerConfiguration } from 'vs/platform/sharedProcess/com import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { WindowError } from 'vs/platform/window/electron-main/window'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { IPolicyService } from 'vs/platform/policy/common/policy'; export class SharedProcess extends Disposable implements ISharedProcess { @@ -39,6 +40,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService, + @IPolicyService private readonly policyService: IPolicyService, @IThemeMainService private readonly themeMainService: IThemeMainService, @IProtocolMainService private readonly protocolMainService: IProtocolMainService ) { @@ -242,7 +244,8 @@ export class SharedProcess extends Disposable implements ISharedProcess { userEnv: this.userEnv, args: this.environmentMainService.args, logLevel: this.logService.getLevel(), - product + product, + policiesData: this.policyService.serialize() }); // Load with config diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts index 70c028b7a11..5c0265c6c4f 100644 --- a/src/vs/platform/sharedProcess/node/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IStringDictionary } from 'vs/base/common/collections'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { LogLevel } from 'vs/platform/log/common/log'; +import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy'; export interface ISharedProcess { @@ -24,4 +26,6 @@ export interface ISharedProcessConfiguration extends ISandboxConfiguration { readonly logLevel: LogLevel; readonly backupWorkspacesPath: string; + + readonly policiesData?: IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }>; } diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 86737bd1657..42b9e8cee7a 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -57,7 +57,7 @@ export abstract class AbstractUpdateService implements IUpdateService { * optimization, to avoid using extra CPU cycles before first window open. * https://github.com/microsoft/vscode/issues/89784 */ - async initialize(): Promise { + initialize(): void { if (!this.environmentMainService.isBuilt) { return; // updates are never enabled when running out of sources } @@ -72,7 +72,7 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - const updateMode = await this.getUpdateMode(); + const updateMode = this.getUpdateMode(); const quality = this.getProductQuality(updateMode); if (!quality) { @@ -104,7 +104,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } } - protected async getUpdateMode(): Promise<'none' | 'manual' | 'start' | 'default'> { + protected getUpdateMode(): 'none' | 'manual' | 'start' | 'default' { return getMigratedSettingValue<'none' | 'manual' | 'start' | 'default'>(this.configurationService, 'update.mode', 'update.channel'); } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 688ea356ef1..0c008ea8504 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -38,8 +38,8 @@ export class DarwinUpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } - override async initialize(): Promise { - await super.initialize(); + override initialize(): void { + super.initialize(); this.onRawError(this.onError, this, this.disposables); this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index e3661ce4bbc..fa2490cdfca 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -47,14 +47,6 @@ function getUpdateType(): UpdateType { return _updateType; } -function validateUpdateModeValue(value: string | undefined): 'none' | 'manual' | 'start' | 'default' | undefined { - if (value === 'none' || value === 'manual' || value === 'start' || value === 'default') { - return value; - } else { - return undefined; - } -} - export class Win32UpdateService extends AbstractUpdateService { private availableUpdate: IAvailableUpdate | undefined; @@ -79,26 +71,6 @@ export class Win32UpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } - protected override async getUpdateMode(): Promise<'none' | 'manual' | 'start' | 'default'> { - if (this.productService.win32RegValueName) { - const policyKey = `Software\\Policies\\Microsoft\\${this.productService.win32RegValueName}`; - const [hklm, hkcu] = await Promise.all([ - this.nativeHostMainService.windowsGetStringRegKey(undefined, 'HKEY_LOCAL_MACHINE', policyKey, 'UpdateMode').then(validateUpdateModeValue), - this.nativeHostMainService.windowsGetStringRegKey(undefined, 'HKEY_CURRENT_USER', policyKey, 'UpdateMode').then(validateUpdateModeValue) - ]); - - if (hklm) { - this.logService.info(`update#getUpdateMode: 'UpdateMode' policy defined in 'HKLM\\${policyKey}':`, hklm); - return hklm; - } else if (hkcu) { - this.logService.info(`update#getUpdateMode: 'UpdateMode' policy defined in 'HKCU\\${policyKey}':`, hkcu); - return hkcu; - } - } - - return await super.getUpdateMode(); - } - protected buildUpdateFeedUrl(quality: string): string | undefined { let platform = 'win32'; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 293324b83c5..12abf923f28 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -41,6 +41,7 @@ import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/pl import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; export class UserDataSyncClient extends Disposable { @@ -86,7 +87,7 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IStorageService, this._register(new InMemoryStorageService())); - const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService, new NullPolicyService(), logService)); await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); this.instantiationService.stub(IUriIdentityService, this.instantiationService.createInstance(UriIdentityService)); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 2c9591a2bec..afac7f93ce7 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IStringDictionary } from 'vs/base/common/collections'; import { PerformanceMark } from 'vs/base/common/performance'; import { isLinux, isMacintosh, isNative, isWeb } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -12,6 +13,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { FileType } from 'vs/platform/files/common/files'; import { LogLevel } from 'vs/platform/log/common/log'; +import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; @@ -289,6 +291,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native filesToWait?: IPathsToWaitFor; os: IOSConfiguration; + policiesData?: IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }>; } /** diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index dd3fb88731f..3b8ba525827 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -39,6 +39,7 @@ import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderW import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IWindowState, ICodeWindow, ILoadEvent, WindowMode, WindowError, LoadReason, defaultWindowState } from 'vs/platform/window/electron-main/window'; import { Color } from 'vs/base/common/color'; +import { IPolicyService } from 'vs/platform/policy/common/policy'; export interface IWindowCreationOptions { state: IWindowState; @@ -149,6 +150,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { config: IWindowCreationOptions, @ILogService private readonly logService: ILogService, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, + @IPolicyService private readonly policyService: IPolicyService, @IFileService private readonly fileService: IFileService, @IGlobalStorageMainService private readonly globalStorageMainService: IGlobalStorageMainService, @IStorageMainService private readonly storageMainService: IStorageMainService, @@ -870,6 +872,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } configuration.isInitialStartup = false; // since this is a reload + configuration.policiesData = this.policyService.serialize(); // set policies data again // Load config this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] }); diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index ec6d1848956..81ecd35e990 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -51,6 +51,7 @@ import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electro import { ICodeWindow, UnloadReason } from 'vs/platform/window/electron-main/window'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IPolicyService } from 'vs/platform/policy/common/policy'; //#region Helper Interfaces @@ -192,6 +193,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, @IStateMainService private readonly stateMainService: IStateMainService, + @IPolicyService private readonly policyService: IPolicyService, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @@ -1320,7 +1322,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic autoDetectHighContrast: windowConfig?.autoDetectHighContrast ?? true, autoDetectColorScheme: windowConfig?.autoDetectColorScheme ?? false, accessibilitySupport: app.accessibilitySupportEnabled, - colorScheme: this.themeMainService.getColorScheme() + colorScheme: this.themeMainService.getColorScheme(), + policiesData: this.policyService.serialize(), }; let window: ICodeWindow | undefined; diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index f1e8986f5c5..36c01538db4 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -42,6 +42,7 @@ import { buildHelpMessage, buildVersionMessage, OptionDescriptions } from 'vs/pl import { isWindows } from 'vs/base/common/platform'; import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsScannerService } from 'vs/server/node/extensionsScannerService'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; class CliMain extends Disposable { @@ -91,7 +92,7 @@ class CliMain extends Disposable { fileService.registerProvider(Schemas.file, this._register(new DiskFileSystemProvider(logService))); // Configuration - const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService, new NullPolicyService(), logService)); await configurationService.initialize(); services.set(IConfigurationService, configurationService); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index a072fa8bb0e..9885e9a1982 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -70,6 +70,7 @@ import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/co import { ExtensionHostStatusService, IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsScannerService } from 'vs/server/node/extensionsScannerService'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; const eventPrefix = 'monacoworkbench'; @@ -107,7 +108,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IFileService, fileService); fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService))); - const configurationService = new ConfigurationService(environmentService.machineSettingsResource, fileService); + const configurationService = new ConfigurationService(environmentService.machineSettingsResource, fileService, new NullPolicyService(), logService); services.set(IConfigurationService, configurationService); const extensionHostStatusService = new ExtensionHostStatusService(); diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 406661573d2..17d42a088fa 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -204,7 +204,8 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { configuration.properties = {}; } for (let key in properties) { - const message = validateProperty(key); + const propertyConfiguration = properties[key]; + const message = validateProperty(key, propertyConfiguration); if (message) { delete properties[key]; extension.collector.warn(message); @@ -215,7 +216,6 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { extension.collector.warn(nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", key)); continue; } - const propertyConfiguration = properties[key]; if (!isObject(propertyConfiguration)) { delete properties[key]; extension.collector.error(nls.localize('invalid.property', "configuration.properties property '{0}' must be an object", key)); diff --git a/src/vs/workbench/api/common/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts index 3a45d62f28f..0bd5f8e9752 100644 --- a/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -256,7 +256,7 @@ export class ExtHostConfigProvider { return { key, - defaultValue: config.default?.value, + defaultValue: config.policy?.value ?? config.default?.value, globalValue: config.user?.value, workspaceValue: config.workspace?.value, workspaceFolderValue: config.workspaceFolder?.value, diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index 764c1c294b1..950c278a1ff 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -43,6 +43,7 @@ suite('ExtHostConfiguration', function () { function createConfigurationData(contents: any): IConfigurationInitData { return { defaults: new ConfigurationModel(contents), + policy: new ConfigurationModel(), user: new ConfigurationModel(contents), workspace: new ConfigurationModel(), folders: [], @@ -279,6 +280,7 @@ suite('ExtHostConfiguration', function () { 'wordWrap': 'off' } }, ['editor.wordWrap']), + policy: new ConfigurationModel(), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' @@ -328,6 +330,7 @@ suite('ExtHostConfiguration', function () { 'wordWrap': 'off' } }, ['editor.wordWrap']), + policy: new ConfigurationModel(), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' @@ -405,6 +408,7 @@ suite('ExtHostConfiguration', function () { 'lineNumbers': 'on' } }, ['editor.wordWrap']), + policy: new ConfigurationModel(), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' @@ -508,6 +512,7 @@ suite('ExtHostConfiguration', function () { 'editor.wordWrap': 'bounded', } }), + policy: new ConfigurationModel(), user: toConfigurationModel({ 'editor.wordWrap': 'bounded', '[typescript]': { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index b8dd198abc8..b8a397edac4 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -73,6 +73,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { DelayedLogChannel } from 'vs/workbench/services/output/common/delayedLogChannel'; import { dirname, joinPath } from 'vs/base/common/resources'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; export class BrowserMain extends Disposable { @@ -440,7 +441,7 @@ export class BrowserMain extends Disposable { private async createWorkspaceService(payload: IAnyWorkspaceIdentifier, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise { const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData, Schemas.tmp] /* Cache all non native resources */, environmentService, fileService); - const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService); + const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService, new NullPolicyService()); try { await workspaceService.initialize(payload); diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 92733560d7b..7b4684b2961 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -399,13 +399,19 @@ -webkit-user-select: text; } -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-untrusted > .setting-item-contents .setting-item-trust-description { +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-untrusted > .setting-item-contents .setting-item-trust-description, +.settings-editor > .settings-body .settings-tree-container .setting-item > .setting-item-contents .setting-item-policy-description { display: flex; font-weight: 600; margin: 6px 0 12px 0; } -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-untrusted > .setting-item-contents .setting-item-trust-description > span { +.settings-editor > .settings-body .settings-tree-container .setting-item > .setting-item-contents .setting-item-policy-description[hidden] { + display: none; +} + +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-untrusted > .setting-item-contents .setting-item-trust-description > span, +.settings-editor > .settings-body .settings-tree-container .setting-item > .setting-item-contents .setting-item-policy-description > span { padding-right: 5px; } @@ -413,6 +419,10 @@ color: var(--workspace-trust-state-untrusted-color) !important; } +.settings-editor > .settings-body .settings-tree-container .setting-item > .setting-item-contents .setting-item-policy-description > span.codicon.codicon-lock { + color: var(--organization-policy-icon-color) !important; +} + .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-validation-message { display: none; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 3bb3a346bd3..a2aa81b94c1 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -175,6 +175,7 @@ class EditSettingRenderer extends Disposable { constructor(private editor: ICodeEditor, private primarySettingsModel: ISettingsEditorModel, private settingHighlighter: SettingHighlighter, + @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { @@ -283,6 +284,9 @@ class EditSettingRenderer extends Disposable { return this.getSettingsAtLineNumber(lineNumber).filter(setting => { const configurationNode = configurationMap[setting.key]; if (configurationNode) { + if (configurationNode.policy && this.configurationService.inspect(setting.key).policyValue !== undefined) { + return false; + } if (this.isDefaultSettings()) { if (setting.key === 'launch') { // Do not show because of https://github.com/microsoft/vscode/issues/32593 @@ -522,6 +526,9 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc for (const setting of section.settings) { const configuration = configurationRegistry[setting.key]; if (configuration) { + if (this.handlePolicyConfiguration(setting, configuration, markerData)) { + continue; + } switch (this.settingsEditorModel.configurationTarget) { case ConfigurationTarget.USER_LOCAL: this.handleLocalUserConfiguration(setting, configuration, markerData); @@ -550,6 +557,25 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc return markerData; } + private handlePolicyConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): boolean { + if (!configuration.policy) { + return false; + } + if (this.configurationService.inspect(setting.key).policyValue === undefined) { + return false; + } + if (this.settingsEditorModel.configurationTarget === ConfigurationTarget.DEFAULT) { + return false; + } + markerData.push({ + severity: MarkerSeverity.Hint, + tags: [MarkerTag.Unnecessary], + ...setting.range, + message: nls.localize('unsupportedPolicySetting', "This setting cannot be applied because it is configured in the system policy.") + }); + return true; + } + private handleLocalUserConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): void { if (this.environmentService.remoteAuthority && (configuration.scope === ConfigurationScope.MACHINE || configuration.scope === ConfigurationScope.MACHINE_OVERRIDABLE)) { markerData.push({ diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index d2d2c183503..4bbcafeb7c2 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -43,7 +43,7 @@ import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/brow import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; @@ -126,6 +126,7 @@ export class SettingsEditor2 extends EditorPane { `@${FEATURE_SETTING_TAG}remote`, `@${FEATURE_SETTING_TAG}timeline`, `@${FEATURE_SETTING_TAG}notebook`, + `@${POLICY_SETTING_TAG}` ]; private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean { @@ -866,10 +867,10 @@ export class SettingsEditor2 extends EditorPane { // the element was not found } })); - this._register(this.settingRenderers.onApplyLanguageFilter((lang: string) => { - if (this.searchWidget) { - // Prepend the language filter to the query. - const newQuery = `@${LANGUAGE_SETTING_TAG}${lang} ${this.searchWidget.getValue().trimStart()}`; + this._register(this.settingRenderers.onApplyFilter((filter: string) => { + if (this.searchWidget && !this.searchWidget.getValue().includes(filter)) { + // Prepend the filter to the query. + const newQuery = `${filter} ${this.searchWidget.getValue().trimStart()}`; this.focusSearch(newQuery, false); } })); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts index e39d23eb58e..9a1c0ec289c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts @@ -10,7 +10,7 @@ import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestCont import { localize } from 'vs/nls'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; -import { EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, GENERAL_TAG_SETTING_TAG, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, GENERAL_TAG_SETTING_TAG, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionViewItem { private readonly suggestController: SuggestController | null; @@ -135,6 +135,12 @@ export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenu localize('onlineSettingsSearch', "Online services"), localize('onlineSettingsSearchTooltip', "Show settings for online services"), '@tag:usesOnlineServices' + ), + this.createToggleAction( + 'policySettingsSearch', + localize('policySettingsSearch', "Policy services"), + localize('policySettingsSearchTooltip', "Show settings for policy services"), + `@${POLICY_SETTING_TAG}` ) ]; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 7b551ab1466..2e2ee4e7257 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -36,14 +36,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorBackground, editorErrorForeground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, editorErrorForeground, editorInfoForeground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler, attachStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { inspectSetting, ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, ObjectSettingDropdownWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester, ObjectSettingCheckboxWidget } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; -import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; +import { LANGUAGE_SETTING_TAG, POLICY_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { getDefaultIgnoredSettings, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; @@ -584,6 +584,7 @@ interface ISettingItemTemplate extends IDisposableTemplate { containerElement: HTMLElement; categoryElement: HTMLElement; labelElement: SimpleIconLabel; + policyWarningElement: HTMLElement; descriptionElement: HTMLElement; controlElement: HTMLElement; deprecationWarningElement: HTMLElement; @@ -605,6 +606,7 @@ type ISettingNumberItemTemplate = ISettingTextItemTemplate; interface ISettingEnumItemTemplate extends ISettingItemTemplate { selectBox: SelectBox; + selectElement: HTMLSelectElement | null; enumDescriptionElement: HTMLElement; } @@ -738,8 +740,8 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre protected readonly _onDidChangeSettingHeight = this._register(new Emitter()); readonly onDidChangeSettingHeight: Event = this._onDidChangeSettingHeight.event; - protected readonly _onApplyLanguageFilter = this._register(new Emitter()); - readonly onApplyLanguageFilter: Event = this._onApplyLanguageFilter.event; + protected readonly _onApplyFilter = this._register(new Emitter()); + readonly onApplyFilter: Event = this._onApplyFilter.event; private readonly markdownRenderer: MarkdownRenderer; @@ -781,9 +783,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const categoryElement = DOM.append(labelCategoryContainer, $('span.setting-item-category')); const labelElementContainer = DOM.append(labelCategoryContainer, $('span.setting-item-label')); const labelElement = new SimpleIconLabel(labelElementContainer); - const miscLabel = new SettingsTreeMiscLabel(titleElement); - const descriptionElement = DOM.append(container, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); modifiedIndicatorElement.title = localize('modified', "The setting has been configured in the current scope."); @@ -794,6 +794,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); const toDispose = new DisposableStore(); + const policyWarningElement = this.renderPolicyLabel(container, toDispose); const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); const toolbar = this.renderSettingToolbar(toolbarContainer); @@ -805,6 +806,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre containerElement: container, categoryElement, labelElement, + policyWarningElement, descriptionElement, controlElement, deprecationWarningElement, @@ -839,6 +841,33 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre }); } + protected renderPolicyLabel(container: HTMLElement, toDispose: DisposableStore): HTMLElement { + const policyWarningElement = DOM.append(container, $('.setting-item-policy-description')); + const policyIcon = DOM.append(policyWarningElement, $('span.codicon.codicon-lock')); + toDispose.add(attachStylerCallback(this._themeService, { editorInfoForeground }, colors => { + policyIcon.style.setProperty('--organization-policy-icon-color', colors.editorInfoForeground?.toString() || ''); + })); + const element = DOM.append(policyWarningElement, $('span')); + element.textContent = localize('policyLabel', "This setting is managed by your organization."); + const viewPolicyLabel = localize('viewPolicySettings', "View policy settings"); + const linkElement: HTMLAnchorElement = DOM.append(policyWarningElement, $('a')); + linkElement.textContent = viewPolicyLabel; + linkElement.setAttribute('tabindex', '0'); + linkElement.href = '#'; + toDispose.add(DOM.addStandardDisposableListener(linkElement, DOM.EventType.CLICK, (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this._onApplyFilter.fire(`@${POLICY_SETTING_TAG}`); + })); + toDispose.add(DOM.addStandardDisposableListener(linkElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) { + e.stopPropagation(); + this._onApplyFilter.fire(`@${POLICY_SETTING_TAG}`); + } + })); + return policyWarningElement; + } + protected renderSettingToolbar(container: HTMLElement): ToolBar { const toggleMenuKeybinding = this._keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU); let toggleMenuTitle = localize('settingsContextMenuTitle', "More Actions... "); @@ -908,6 +937,8 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.miscLabel.updateSyncIgnored(element, this.ignoredSettings); })); + template.policyWarningElement.hidden = !element.hasPolicyValue; + this.updateSettingTabbable(element, template); template.elementDisposables.add(element.onDidChangeTabbable(() => { this.updateSettingTabbable(element, template); @@ -1087,7 +1118,7 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I template.elementDisposables.add(template.button.onDidClick(() => { if (isLanguageTagSetting) { - this._onApplyLanguageFilter.fire(plainKey); + this._onApplyFilter.fire(`@${LANGUAGE_SETTING_TAG}:${plainKey}`); } else { this._onDidOpenSettings.fire(dataElement.setting.key); } @@ -1538,6 +1569,7 @@ abstract class AbstractSettingTextRenderer extends AbstractSettingRenderer imple template.onChange = undefined; template.inputBox.value = dataElement.value; template.inputBox.setAriaLabel(dataElement.setting.key); + template.inputBox.inputElement.disabled = !!dataElement.hasPolicyValue; template.onChange = value => { if (!renderValidations(dataElement, template, false)) { onChange(value); @@ -1633,6 +1665,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const template: ISettingEnumItemTemplate = { ...common, selectBox, + selectElement, enumDescriptionElement }; @@ -1704,6 +1737,10 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre } }; + if (template.selectElement) { + template.selectElement.disabled = !!dataElement.hasPolicyValue; + } + template.enumDescriptionElement.innerText = ''; } } @@ -1757,6 +1794,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT template.onChange = undefined; template.inputBox.value = dataElement.value; template.inputBox.setAriaLabel(dataElement.setting.key); + template.inputBox.setEnabled(!dataElement.hasPolicyValue); template.onChange = value => { if (!renderValidations(dataElement, template, false)) { onChange(nullNumParseFn(value)); @@ -1789,7 +1827,6 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); modifiedIndicatorElement.title = localize('modified', "The setting has been configured in the current scope."); - const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); const toDispose = new DisposableStore(); @@ -1819,6 +1856,8 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const toolbar = this.renderSettingToolbar(toolbarContainer); toDispose.add(toolbar); + const policyWarningElement = this.renderPolicyLabel(container, toDispose); + const template: ISettingBoolItemTemplate = { toDispose, elementDisposables: new DisposableStore(), @@ -1828,6 +1867,7 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre labelElement, controlElement, checkbox, + policyWarningElement, descriptionElement, deprecationWarningElement, miscLabel, @@ -1852,6 +1892,11 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre template.onChange = undefined; template.checkbox.checked = dataElement.value; template.checkbox.setTitle(dataElement.setting.key); + if (dataElement.hasPolicyValue) { + template.checkbox.disable(); + } else { + template.checkbox.enable(); + } template.onChange = onChange; } } @@ -1912,7 +1957,7 @@ export class SettingTreeRenderers { readonly onDidChangeSettingHeight: Event; - readonly onApplyLanguageFilter: Event; + readonly onApplyFilter: Event; readonly allRenderers: ITreeRenderer[]; @@ -1961,7 +2006,7 @@ export class SettingTreeRenderers { this.onDidClickSettingLink = Event.any(...settingRenderers.map(r => r.onDidClickSettingLink)); this.onDidFocusSetting = Event.any(...settingRenderers.map(r => r.onDidFocusSetting)); this.onDidChangeSettingHeight = Event.any(...settingRenderers.map(r => r.onDidChangeSettingHeight)); - this.onApplyLanguageFilter = Event.any(...settingRenderers.map(r => r.onApplyLanguageFilter)); + this.onApplyFilter = Event.any(...settingRenderers.map(r => r.onApplyFilter)); this.allRenderers = [ ...settingRenderers, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index e41f7298554..b16ea3ac942 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -11,7 +11,7 @@ import { localize } from 'vs/nls'; import { ConfigurationTarget, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { ITOCEntry, knownAcronyms, knownTermMappings, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { IExtensionSetting, ISearchResult, ISetting, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; @@ -139,6 +139,11 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { */ isUntrusted = false; + /** + * Whether the setting is under a policy that blocks all changes. + */ + hasPolicyValue = false; + tags?: Set; overriddenScopeList: string[] = []; languageOverrideValues: Map> = new Map>(); @@ -182,7 +187,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } update(inspectResult: IInspectResult, isWorkspaceTrusted: boolean): void { - const { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector } = inspectResult; + let { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector } = inspectResult; switch (targetSelector) { case 'workspaceFolderValue': @@ -214,12 +219,17 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } } - if (languageSelector && this.languageOverrideValues.has(languageSelector)) { + if (inspected.policyValue) { + this.hasPolicyValue = true; + isConfigured = false; // The user did not manually configure the setting themselves. + displayValue = inspected.policyValue; + this.scopeValue = inspected.policyValue; + this.defaultValue = inspected.defaultValue; + } else if (languageSelector && this.languageOverrideValues.has(languageSelector)) { const overrideValues = this.languageOverrideValues.get(languageSelector)!; // In the worst case, go back to using the previous display value. // Also, sometimes the override is in the form of a default value override, so consider that second. displayValue = (isConfigured ? overrideValues[targetSelector] : overrideValues.defaultValue) ?? displayValue; - this.value = displayValue; this.scopeValue = isConfigured && overrideValues[targetSelector]; this.defaultValue = overrideValues.defaultValue ?? inspected.defaultValue; @@ -229,13 +239,13 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { this.setting.defaultValueSource = overrideValueSource; } } else { - this.value = displayValue; this.scopeValue = isConfigured && inspected[targetSelector]; this.defaultValue = inspected.defaultValue; } + this.value = displayValue; this.isConfigured = isConfigured; - if (isConfigured || this.setting.tags || this.tags || this.setting.restricted) { + if (isConfigured || this.setting.tags || this.tags || this.setting.restricted || this.hasPolicyValue) { // Don't create an empty Set for all 1000 settings, only if needed this.tags = new Set(); if (isConfigured) { @@ -249,6 +259,10 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { if (this.setting.restricted) { this.tags.add(REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG); } + + if (this.hasPolicyValue) { + this.tags.add(POLICY_SETTING_TAG); + } } this.overriddenScopeList = overriddenScopeList; @@ -902,6 +916,11 @@ export function parseQuery(query: string): IParsedQuery { return ''; }); + query = query.replace(`@${POLICY_SETTING_TAG}`, () => { + tags.push(POLICY_SETTING_TAG); + return ''; + }); + const extensions: string[] = []; const features: string[] = []; const ids: string[] = []; diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index 6138098d36f..6b87d1ae46b 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -78,6 +78,7 @@ export const FEATURE_SETTING_TAG = 'feature:'; export const ID_SETTING_TAG = 'id:'; export const LANGUAGE_SETTING_TAG = 'lang:'; export const GENERAL_TAG_SETTING_TAG = 'tag:'; +export const POLICY_SETTING_TAG = 'hasPolicy'; export const WORKSPACE_TRUST_SETTING_TAG = 'workspaceTrust'; export const REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG = 'requireTrustedWorkspace'; export const KEYBOARD_LAYOUT_OPEN_PICKER = 'workbench.action.openKeyboardLayoutPicker'; diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 98b95174864..88955896dce 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -50,6 +50,8 @@ import { isCI, isMacintosh } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/workbench/services/files/electron-sandbox/diskFileSystemProvider'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; export class DesktopMain extends Disposable { @@ -155,6 +157,10 @@ export class DesktopMain extends Disposable { const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId)); serviceCollection.set(IMainProcessService, mainProcessService); + // Policies + const policyService = this.configuration.policiesData ? new PolicyChannelClient(this.configuration.policiesData, mainProcessService.getChannel('policy')) : new NullPolicyService(); + serviceCollection.set(IPolicyService, policyService); + // Product const productService: IProductService = { _serviceBrand: undefined, ...product }; serviceCollection.set(IProductService, productService); @@ -247,7 +253,7 @@ export class DesktopMain extends Disposable { const payload = this.resolveWorkspaceInitializationPayload(environmentService); const [configurationService, storageService] = await Promise.all([ - this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, uriIdentityService, logService).then(service => { + this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, uriIdentityService, logService, policyService).then(service => { // Workspace serviceCollection.set(IWorkspaceContextService, service); @@ -325,9 +331,17 @@ export class DesktopMain extends Disposable { return workspaceInitializationPayload; } - private async createWorkspaceService(payload: IAnyWorkspaceIdentifier, environmentService: INativeWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise { + private async createWorkspaceService( + payload: IAnyWorkspaceIdentifier, + environmentService: INativeWorkbenchEnvironmentService, + fileService: FileService, + remoteAgentService: IRemoteAgentService, + uriIdentityService: IUriIdentityService, + logService: ILogService, + policyService: IPolicyService + ): Promise { const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData] /* Cache all non native resources */, environmentService, fileService); - const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService); + const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService, policyService); try { await workspaceService.initialize(payload); diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 2bd9b092c9a..39ed9b5e64c 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -9,7 +9,7 @@ import * as errors from 'vs/base/common/errors'; import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult, FileOperation, FileOperationEvent } from 'vs/platform/files/common/files'; -import { ConfigurationModel, ConfigurationModelParser, ConfigurationParseOptions, DefaultConfigurationModel, UserSettings } from 'vs/platform/configuration/common/configurationModels'; +import { ConfigurationModel, ConfigurationModelParser, ConfigurationParseOptions, UserSettings } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; @@ -26,8 +26,9 @@ import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { isObject } from 'vs/base/common/types'; +import { DefaultConfiguration as BaseDefaultConfiguration } from 'vs/platform/configuration/common/configurations'; -export class DefaultConfiguration extends Disposable { +export class DefaultConfiguration extends BaseDefaultConfiguration { static readonly DEFAULT_OVERRIDES_CACHE_EXISTS_KEY = 'DefaultOverridesCacheExists'; @@ -35,9 +36,6 @@ export class DefaultConfiguration extends Disposable { private cachedConfigurationDefaultsOverrides: IStringDictionary = {}; private readonly cacheKey: ConfigurationKey = { type: 'defaults', key: 'configurationDefaultsOverrides' }; - private readonly _onDidChangeConfiguration = this._register(new Emitter<{ defaults: ConfigurationModel; properties: string[] }>()); - readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; - private updateCache: boolean = false; constructor( @@ -50,27 +48,20 @@ export class DefaultConfiguration extends Disposable { } } - private _configurationModel: ConfigurationModel | undefined; - get configurationModel(): ConfigurationModel { - if (!this._configurationModel) { - this._configurationModel = new DefaultConfigurationModel(this.cachedConfigurationDefaultsOverrides); - } - return this._configurationModel; + protected override getConfigurationDefaultOverrides(): IStringDictionary { + return this.cachedConfigurationDefaultsOverrides; } - async initialize(): Promise { + override async initialize(): Promise { await this.initializeCachedConfigurationDefaultsOverrides(); - this._configurationModel = undefined; - this._register(this.configurationRegistry.onDidUpdateConfiguration(({ properties, defaultsOverrides }) => this.onDidUpdateConfiguration(properties, defaultsOverrides))); - return this.configurationModel; + return super.initialize(); } - reload(): ConfigurationModel { + override reload(): ConfigurationModel { this.updateCache = true; this.cachedConfigurationDefaultsOverrides = {}; - this._configurationModel = undefined; this.updateCachedConfigurationDefaultsOverrides(); - return this.configurationModel; + return super.reload(); } private initiaizeCachedConfigurationDefaultsOverridesPromise: Promise | undefined; @@ -92,9 +83,8 @@ export class DefaultConfiguration extends Disposable { return this.initiaizeCachedConfigurationDefaultsOverridesPromise; } - private onDidUpdateConfiguration(properties: string[], defaultsOverrides?: boolean): void { - this._configurationModel = undefined; - this._onDidChangeConfiguration.fire({ defaults: this.configurationModel, properties }); + protected override onDidUpdateConfiguration(properties: string[], defaultsOverrides?: boolean): void { + super.onDidUpdateConfiguration(properties, defaultsOverrides); if (defaultsOverrides) { this.updateCachedConfigurationDefaultsOverrides(); } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 45af17cc175..d32d7e978bb 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -13,6 +13,7 @@ import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/plat import { IWorkspaceContextService, Workspace as BaseWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkspaceFolder, toWorkspaceFolder, isWorkspaceFolder, IWorkspaceFoldersWillChangeEvent, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { ConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent, mergeChanges } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange, ConfigurationTargetToString, IConfigurationUpdateOverrides, isConfigurationUpdateOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; import { Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, RestrictedSettings } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -39,6 +40,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { isUndefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; class Workspace extends BaseWorkspace { initialized: boolean = false; @@ -54,7 +56,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private readonly configurationCache: IConfigurationCache; private _configuration: Configuration; private initialized: boolean = false; - private defaultConfiguration: DefaultConfiguration; + private readonly defaultConfiguration: DefaultConfiguration; + private readonly policyConfiguration: IPolicyConfiguration; private localUserConfiguration: UserConfiguration; private remoteUserConfiguration: RemoteUserConfiguration | null = null; private workspaceConfiguration: WorkspaceConfiguration; @@ -102,6 +105,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService, + policyService: IPolicyService ) { super(); @@ -109,12 +113,13 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat this.initRemoteUserConfigurationBarrier = new Barrier(); this.completeWorkspaceBarrier = new Barrier(); - this.defaultConfiguration = new DefaultConfiguration(configurationCache, environmentService); + this.defaultConfiguration = this._register(new DefaultConfiguration(configurationCache, environmentService)); + this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService)); this.configurationCache = configurationCache; this.fileService = fileService; this.uriIdentityService = uriIdentityService; this.logService = logService; - this._configuration = new Configuration(this.defaultConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), this.workspace); + this._configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), this.workspace); this.cachedFolderConfigs = new ResourceMap(); this.localUserConfiguration = this._register(new UserConfiguration(environmentService.settingsResource, remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, fileService, uriIdentityService, logService)); this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); @@ -138,6 +143,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat })); this._register(this.defaultConfiguration.onDidChangeConfiguration(({ properties, defaults }) => this.onDefaultConfigurationChanged(defaults, properties))); + this._register(this.policyConfiguration.onDidChangeConfiguration(configurationModel => this.onPolicyConfigurationChanged(configurationModel))); this.workspaceEditingQueue = new Queue(); } @@ -338,6 +344,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat async reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise { if (target === undefined) { + this.reloadDefaultConfiguration(); const { local, remote } = await this.reloadUserConfiguration(); await this.reloadWorkspaceConfiguration(); await this.loadConfiguration(local, remote); @@ -351,7 +358,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat switch (target) { case ConfigurationTarget.DEFAULT: - await this.reloadDefaultConfiguration(); + this.reloadDefaultConfiguration(); return; case ConfigurationTarget.USER: { @@ -572,12 +579,18 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private async initializeConfiguration(): Promise { await this.defaultConfiguration.initialize(); - mark('code/willInitUserConfiguration'); - const { local, remote } = await this.initializeUserConfiguration(); - mark('code/didInitUserConfiguration'); + const [, user] = await Promise.all([ + this.policyConfiguration.initialize(), + (async () => { + mark('code/willInitUserConfiguration'); + const result = await this.initializeUserConfiguration(); + mark('code/didInitUserConfiguration'); + return result; + })() + ]); mark('code/willInitWorkspaceConfiguration'); - await this.loadConfiguration(local, remote); + await this.loadConfiguration(user.local, user.remote); mark('code/didInitWorkspaceConfiguration'); } @@ -586,7 +599,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat return { local, remote }; } - private async reloadDefaultConfiguration(): Promise { + private reloadDefaultConfiguration(): void { this.onDefaultConfigurationChanged(this.defaultConfiguration.reload()); } @@ -640,7 +653,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat folderConfigurations.forEach((folderConfiguration, index) => folderConfigurationModels.set(folders[index].uri, folderConfiguration)); const currentConfiguration = this._configuration; - this._configuration = new Configuration(this.defaultConfiguration.configurationModel, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap(), this.workspace); + this._configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap(), this.workspace); if (this.initialized) { const change = this._configuration.compare(currentConfiguration); @@ -692,6 +705,12 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } } + private onPolicyConfigurationChanged(policyConfiguration: ConfigurationModel): void { + const previous = { data: this._configuration.toData(), workspace: this.workspace }; + const change = this._configuration.compareAndUpdatePolicyConfiguration(policyConfiguration); + this.triggerConfigurationChange(change, previous, ConfigurationTarget.DEFAULT); + } + private onLocalUserConfigurationChanged(userConfiguration: ConfigurationModel): void { const previous = { data: this._configuration.toData(), workspace: this.workspace }; const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration); diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 3e4e805c231..bf0d5f3adde 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -92,6 +92,11 @@ export const enum ConfigurationEditingErrorCode { */ ERROR_INVALID_CONFIGURATION, + /** + * Error when trying to write a policy configuration + */ + ERROR_POLICY_CONFIGURATION, + /** * Internal Error. */ @@ -359,6 +364,7 @@ export class ConfigurationEditingService { switch (error) { // API constraints + case ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION: return nls.localize('errorPolicyConfiguration', "Unable to write {0} because it is configured in system policy.", operation.key); case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", this.stringifyTarget(target), operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION: return nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE: return nls.localize('errorInvalidWorkspaceConfigurationMachine', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); @@ -492,6 +498,10 @@ export class ConfigurationEditingService { private async validate(target: EditableConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationUpdateOverrides): Promise { + if (this.configurationService.inspect(operation.key).policyValue !== undefined) { + throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION, target, operation); + } + const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); const configurationScope = configurationProperties[operation.key]?.scope; diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 95a1d40ad77..807e96399df 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -98,6 +98,7 @@ export class Configuration extends BaseConfiguration { constructor( defaults: ConfigurationModel, + policy: ConfigurationModel, localUser: ConfigurationModel, remoteUser: ConfigurationModel, workspaceConfiguration: ConfigurationModel, @@ -105,7 +106,7 @@ export class Configuration extends BaseConfiguration { memoryConfiguration: ConfigurationModel, memoryConfigurationByResource: ResourceMap, private readonly _workspace?: Workspace) { - super(defaults, localUser, remoteUser, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); + super(defaults, policy, localUser, remoteUser, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); } override getValue(key: string | undefined, overrides: IConfigurationOverrides = {}): any { diff --git a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts index 7d7e3cb1267..995442cd8b8 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts @@ -6,6 +6,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; import * as json from 'vs/base/common/json'; +import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -37,8 +38,11 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { joinPath } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentService'; -import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { getSingleFolderWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { hash } from 'vs/base/common/hash'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -52,7 +56,7 @@ export class ConfigurationCache implements IConfigurationCache { suite('ConfigurationEditingService', () => { let instantiationService: TestInstantiationService; - let environmentService: BrowserWorkbenchEnvironmentService; + let environmentService: IWorkbenchEnvironmentService; let fileService: IFileService; let workspaceService: WorkspaceService; let testObject: ConfigurationEditingService; @@ -76,6 +80,14 @@ suite('ConfigurationEditingService', () => { 'configurationEditing.service.testSettingThree': { 'type': 'string', 'default': 'isSet' + }, + 'configurationEditing.service.policySetting': { + 'type': 'string', + 'default': 'isSet', + policy: { + name: 'configurationEditing.service.policySetting', + minimumVersion: '1.0.0', + } } } }); @@ -92,12 +104,17 @@ suite('ConfigurationEditingService', () => { instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(workspaceFolder, 'policies.json'); instantiationService.stub(IEnvironmentService, environmentService); const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService)))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IRemoteAgentService, remoteAgentService); - workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); + await workspaceService.initialize({ + id: hash(workspaceFolder.toString()).toString(16), + uri: workspaceFolder + }); instantiationService.stub(IWorkspaceContextService, workspaceService); await workspaceService.initialize(getSingleFolderWorkspaceIdentifier(workspaceFolder)); @@ -180,6 +197,28 @@ suite('ConfigurationEditingService', () => { assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'); }); + test('errors cases - ERROR_POLICY_CONFIGURATION', async () => { + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(instantiationService.get(IConfigurationService).onDidChangeConfiguration); + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationEditing.service.policySetting": "policyValue" }')); + await promise; + }); + try { + await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.policySetting', value: 'value' }, { donotNotifyError: true }); + } catch (error) { + assert.strictEqual(error.code, ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION); + return; + } + assert.fail('Should fail with ERROR_POLICY_CONFIGURATION'); + }); + + test('write policy setting - when not set', async () => { + await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.policySetting', value: 'value' }, { donotNotifyError: true }); + const contents = await fileService.readFile(environmentService.settingsResource); + const parsed = json.parse(contents.value.toString()); + assert.strictEqual(parsed['configurationEditing.service.policySetting'], 'value'); + }); + test('write one setting - empty file', async () => { await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }); const contents = await fileService.readFile(environmentService.settingsResource); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 352fd8e9d7e..d36be57c247 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -44,6 +44,9 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteA import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; import { hash } from 'vs/base/common/hash'; import { TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; function convertToWorkspacePayload(folder: URI): ISingleFolderWorkspaceIdentifier { return { @@ -77,7 +80,7 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); }); @@ -117,7 +120,7 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a')); @@ -137,7 +140,7 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); @@ -184,7 +187,7 @@ suite('WorkspaceContextService - Workspace', () => { const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -242,7 +245,7 @@ suite('WorkspaceContextService - Workspace Editing', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); @@ -485,7 +488,7 @@ suite('WorkspaceService - Initialization', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -669,7 +672,7 @@ suite('WorkspaceService - Initialization', () => { suite('WorkspaceConfigurationService - Folder', () => { - let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService; + let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IWorkbenchEnvironmentService; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); const disposables: DisposableStore = new DisposableStore(); @@ -708,6 +711,14 @@ suite('WorkspaceConfigurationService - Folder', () => { 'default': 'isSet', restricted: true }, + 'configurationService.folder.policySetting': { + 'type': 'string', + 'default': 'isSet', + policy: { + name: 'configurationService.folder.policySetting', + minimumVersion: '1.0.0', + } + }, } }); @@ -731,10 +742,11 @@ suite('WorkspaceConfigurationService - Folder', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(folder, 'policies.json'); const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -750,7 +762,7 @@ suite('WorkspaceConfigurationService - Folder', () => { teardown(() => disposables.clear()); test('defaults', () => { - assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet' } }); + assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet', 'policySetting': 'isSet' } }); }); test('globals override defaults', async () => { @@ -956,6 +968,25 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); }); + test('policy value override all', async () => { + const result = await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationService.folder.policySetting": "policyValue" }')); + return promise; + }); + assert.deepStrictEqual(result.affectedKeys, ['configurationService.folder.policySetting']); + assert.strictEqual(testObject.getValue('configurationService.folder.policySetting'), 'policyValue'); + assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, 'policyValue'); + }); + + test('policy settings when policy value is not set', async () => { + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.policySetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.policySetting": "workspaceValue" }')); + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.policySetting'), 'workspaceValue'); + assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, undefined); + }); + test('reload configuration emits events after global configuraiton changes', async () => { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); const target = sinon.spy(); @@ -1405,7 +1436,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, workspaceService); @@ -2066,7 +2097,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { const remoteAgentService = instantiationService.stub(IRemoteAgentService, >{ getEnvironment: () => remoteEnvironmentPromise }); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const configurationCache: IConfigurationCache = { read: () => Promise.resolve(''), write: () => Promise.resolve(), remove: () => Promise.resolve(), needsCaching: () => false }; - testObject = disposables.add(new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + testObject = disposables.add(new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); instantiationService.stub(IEnvironmentService, environmentService); diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 228cc13113a..7f391a11ad3 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -155,14 +155,14 @@ suite('Workspace Configuration', () => { test('Test compare same configurations', () => { const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]); - const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); + const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); configuration1.updateDefaultConfiguration(defaultConfigurationModel); configuration1.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration1.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); configuration1.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 })); configuration1.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' })); - const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); + const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); configuration2.updateDefaultConfiguration(defaultConfigurationModel); configuration2.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration2.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); @@ -176,14 +176,14 @@ suite('Workspace Configuration', () => { test('Test compare different configurations', () => { const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]); - const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); + const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); configuration1.updateDefaultConfiguration(defaultConfigurationModel); configuration1.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration1.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); configuration1.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 })); configuration1.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' })); - const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); + const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); configuration2.updateDefaultConfiguration(defaultConfigurationModel); configuration2.updateLocalUserConfiguration(toConfigurationModel({ 'workbench.enableTabs': true, '[typescript]': { 'editor.insertSpaces': true } })); configuration2.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.fontSize': 11 })); diff --git a/yarn.lock b/yarn.lock index 70623ba39e5..e483a32e500 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8127,6 +8127,11 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" +node-addon-api@*: + version "5.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" + integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== + node-addon-api@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" @@ -11782,6 +11787,14 @@ vscode-oniguruma@1.6.1: resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz#2bf4dfcfe3dd2e56eb549a3068c8ee39e6c30ce5" integrity sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ== +vscode-policy-watcher@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vscode-policy-watcher/-/vscode-policy-watcher-1.1.0.tgz#2921353c5080b3452929f1e350b9fab9ff852cc9" + integrity sha512-yPvy3Or66H0l8/FyWbJeGxpWW3GDZf65EIT7fqp1Ethdz4ecEnHThuHti7SlfdRJTf5qnifrX7af02INiHqDMA== + dependencies: + bindings "^1.5.0" + node-addon-api "*" + vscode-proxy-agent@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.12.0.tgz#0775f464b9519b0c903da4dcf50851e1453f4e48"