Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/vs/platform/policy/common/filePolicyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { isObject } from '../../../base/common/types.js';
import { URI } from '../../../base/common/uri.js';
import { FileOperationError, FileOperationResult, IFileService } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';
import { AbstractPolicyService, IPolicyService, PolicyValue } from './policy.js';
import { AbstractPolicyService, IPolicyService, PolicySource, PolicyValue } from './policy.js';

function keysDiff<T>(a: Map<string, T>, b: Map<string, T>): string[] {
const result: string[] = [];
Expand Down Expand Up @@ -75,6 +75,21 @@ export class FilePolicyService extends AbstractPolicyService implements IPolicyS
const diff = keysDiff(this.policies, policies);
this.policies = policies;

// Update metadata for all policies
for (const [key] of policies) {
this.policyMetadata.set(key, {
source: PolicySource.File,
details: `Set via policy file (${this.file.toString()})`
});
}

// Remove metadata for deleted policies
for (const key of this.policyMetadata.keys()) {
if (!policies.has(key)) {
this.policyMetadata.delete(key);
}
}

if (diff.length > 0) {
this._onDidChange.fire(diff);
}
Expand Down
27 changes: 27 additions & 0 deletions src/vs/platform/policy/common/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ export type PolicyDefinition = {
value?: (account: IDefaultAccount) => string | number | boolean | undefined;
};

export enum PolicySource {
/** Policy set via account (e.g., GitHub Copilot account settings) */
Account = 'account',
/** Policy set via device (e.g., Windows Registry, macOS defaults, Linux policy file) */
Device = 'device',
/** Policy set via file (e.g., policy.json) */
File = 'file'
}

export type PolicyMetadata = {
/** The source of the policy */
readonly source: PolicySource;
/** Account session ID if source is Account */
readonly accountSessionId?: string;
/** Organization name if source is Account */
readonly orgName?: string;
/** Additional metadata about the policy source */
readonly details?: string;
};

export const IPolicyService = createDecorator<IPolicyService>('policy');

export interface IPolicyService {
Expand All @@ -25,6 +45,7 @@ export interface IPolicyService {
readonly onDidChange: Event<readonly PolicyName[]>;
updatePolicyDefinitions(policyDefinitions: IStringDictionary<PolicyDefinition>): Promise<IStringDictionary<PolicyValue>>;
getPolicyValue(name: PolicyName): PolicyValue | undefined;
getPolicyMetadata(name: PolicyName): PolicyMetadata | undefined;
serialize(): IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }> | undefined;
readonly policyDefinitions: IStringDictionary<PolicyDefinition>;
}
Expand All @@ -34,6 +55,7 @@ export abstract class AbstractPolicyService extends Disposable implements IPolic

public policyDefinitions: IStringDictionary<PolicyDefinition> = {};
protected policies = new Map<PolicyName, PolicyValue>();
protected policyMetadata = new Map<PolicyName, PolicyMetadata>();

protected readonly _onDidChange = this._register(new Emitter<readonly PolicyName[]>());
readonly onDidChange = this._onDidChange.event;
Expand All @@ -53,6 +75,10 @@ export abstract class AbstractPolicyService extends Disposable implements IPolic
return this.policies.get(name);
}

getPolicyMetadata(name: PolicyName): PolicyMetadata | undefined {
return this.policyMetadata.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)! } }), {});
}
Expand All @@ -65,6 +91,7 @@ export class NullPolicyService implements IPolicyService {
readonly onDidChange = Event.None;
async updatePolicyDefinitions() { return {}; }
getPolicyValue() { return undefined; }
getPolicyMetadata() { return undefined; }
serialize() { return undefined; }
policyDefinitions: IStringDictionary<PolicyDefinition> = {};
}
37 changes: 31 additions & 6 deletions src/vs/platform/policy/common/policyIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,19 @@ import { Event } from '../../../base/common/event.js';
import { DisposableStore } from '../../../base/common/lifecycle.js';
import { PolicyName } from '../../../base/common/policy.js';
import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { AbstractPolicyService, IPolicyService, PolicyDefinition, PolicyValue } from './policy.js';
import { AbstractPolicyService, IPolicyService, PolicyDefinition, PolicyMetadata, PolicySource, PolicyValue } from './policy.js';

interface IPolicyData {
value: PolicyValue | null;
metadata: PolicyMetadata | null;
}

function createDefaultIpcMetadata(): PolicyMetadata {
return {
source: PolicySource.Device,
details: 'Set via IPC channel'
};
}

export class PolicyChannel implements IServerChannel {

Expand All @@ -21,7 +32,13 @@ export class PolicyChannel implements IServerChannel {
switch (event) {
case 'onDidChange': return Event.map(
this.service.onDidChange,
names => names.reduce<object>((r, name) => ({ ...r, [name]: this.service.getPolicyValue(name) ?? null }), {}),
names => names.reduce<object>((r, name) => ({
...r,
[name]: {
value: this.service.getPolicyValue(name) ?? null,
metadata: this.service.getPolicyMetadata(name) ?? null
}
}), {}),
this.disposables
);
}
Expand Down Expand Up @@ -51,16 +68,23 @@ export class PolicyChannelClient extends AbstractPolicyService implements IPolic
this.policyDefinitions[name] = definition;
if (value !== undefined) {
this.policies.set(name, value);
this.policyMetadata.set(name, createDefaultIpcMetadata());
}
}
this.channel.listen<object>('onDidChange')(policies => {
this.channel.listen<IStringDictionary<IPolicyData>>('onDidChange')(policies => {
for (const name in policies) {
const value = policies[name as keyof typeof policies];
const policyData = policies[name];

if (value === null) {
if (policyData.value === null) {
this.policies.delete(name);
this.policyMetadata.delete(name);
} else {
this.policies.set(name, value);
this.policies.set(name, policyData.value);
if (policyData.metadata) {
this.policyMetadata.set(name, policyData.metadata);
} else {
this.policyMetadata.set(name, createDefaultIpcMetadata());
}
}
}

Expand All @@ -72,6 +96,7 @@ export class PolicyChannelClient extends AbstractPolicyService implements IPolic
const result = await this.channel.call<{ [name: PolicyName]: PolicyValue }>('updatePolicyDefinitions', policyDefinitions);
for (const name in result) {
this.policies.set(name, result[name]);
this.policyMetadata.set(name, createDefaultIpcMetadata());
}
}
}
7 changes: 6 additions & 1 deletion src/vs/platform/policy/node/nativePolicyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AbstractPolicyService, IPolicyService, PolicyDefinition, PolicyValue } from '../common/policy.js';
import { AbstractPolicyService, IPolicyService, PolicyDefinition, PolicySource, PolicyValue } from '../common/policy.js';
import { IStringDictionary } from '../../../base/common/collections.js';
import { Throttler } from '../../../base/common/async.js';
import type { PolicyUpdate, Watcher } from '@vscode/policy-watcher';
Expand Down Expand Up @@ -48,8 +48,13 @@ export class NativePolicyService extends AbstractPolicyService implements IPolic

if (value === undefined) {
this.policies.delete(key);
this.policyMetadata.delete(key);
} else {
this.policies.set(key, value);
this.policyMetadata.set(key, {
source: PolicySource.Device,
details: `Set via device policy (${this.productName})`
});
}
}

Expand Down
47 changes: 20 additions & 27 deletions src/vs/workbench/browser/actions/developerActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,47 +783,40 @@ class PolicyDiagnosticsAction extends Action2 {
}
}

// Try to detect where the policy came from
const policySourceMemo = new Map<string, string>();
// Get policy source from metadata
const getPolicySource = (policyName: string): string => {
if (policySourceMemo.has(policyName)) {
return policySourceMemo.get(policyName)!;
}
try {
const policyServiceConstructorName = policyService.constructor.name;
if (policyServiceConstructorName === 'MultiplexPolicyService') {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
const multiplexService = policyService as any;
if (multiplexService.policyServices) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const componentServices = multiplexService.policyServices as ReadonlyArray<any>;
for (const service of componentServices) {
if (service.getPolicyValue && service.getPolicyValue(policyName) !== undefined) {
policySourceMemo.set(policyName, service.constructor.name);
return service.constructor.name;
}
}
}
const metadata = policyService.getPolicyMetadata(policyName);
if (metadata) {
// Map PolicySource enum to user-friendly names
switch (metadata.source) {
case 'account':
return 'Account';
case 'device':
return 'Device';
case 'file':
return 'File';
default:
return metadata.source;
}
return '';
} catch {
return 'Unknown';
}
return 'Unknown';
};

content += '### Applied Policy\n\n';
appliedPolicy.sort((a, b) => getPolicySource(a.name).localeCompare(getPolicySource(b.name)) || a.name.localeCompare(b.name));
if (appliedPolicy.length > 0) {
content += '| Setting Key | Policy Name | Policy Source | Default Value | Current Value | Policy Value |\n';
content += '|-------------|-------------|---------------|---------------|---------------|-------------|\n';
content += '| Setting Key | Policy Name | Policy Source | Source Details | Default Value | Current Value | Policy Value |\n';
content += '|-------------|-------------|---------------|----------------|---------------|---------------|-------------|\n';

for (const setting of appliedPolicy) {
const defaultValue = JSON.stringify(setting.property.default);
const currentValue = JSON.stringify(setting.inspection.value);
const policyValue = JSON.stringify(setting.inspection.policyValue);
const policySource = getPolicySource(setting.name);
const policyMetadata = policyService.getPolicyMetadata(setting.name);
const policySource = policyMetadata ? getPolicySource(setting.name) : 'Unknown';
const sourceDetails = policyMetadata?.details || 'N/A';

content += `| ${setting.key} | ${setting.name} | ${policySource} | \`${defaultValue}\` | \`${currentValue}\` | \`${policyValue}\` |\n`;
content += `| ${setting.key} | ${setting.name} | ${policySource} | ${sourceDetails} | \`${defaultValue}\` | \`${currentValue}\` | \`${policyValue}\` |\n`;
}
content += '\n';
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { IStringDictionary } from '../../../../base/common/collections.js';
import { IDefaultAccount } from '../../../../base/common/defaultAccount.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { AbstractPolicyService, IPolicyService, PolicyDefinition } from '../../../../platform/policy/common/policy.js';
import { AbstractPolicyService, IPolicyService, PolicyDefinition, PolicySource } from '../../../../platform/policy/common/policy.js';
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';


Expand Down Expand Up @@ -41,10 +41,17 @@ export class AccountPolicyService extends AbstractPolicyService implements IPoli
if (policyValue !== undefined) {
if (this.policies.get(key) !== policyValue) {
this.policies.set(key, policyValue);
this.policyMetadata.set(key, {
source: PolicySource.Account,
accountSessionId: this.account?.sessionId,
orgName: this.account?.enterprise ? 'Enterprise' : undefined,
details: `Set via GitHub Copilot account (Session: ${this.account?.sessionId || 'unknown'})`
});
updated.push(key);
}
} else {
if (this.policies.delete(key)) {
this.policyMetadata.delete(key);
updated.push(key);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,20 @@ export class MultiplexPolicyService extends AbstractPolicyService implements IPo

private updatePolicies(): void {
this.policies.clear();
this.policyMetadata.clear();
const updated: string[] = [];
for (const service of this.policyServices) {
const definitions = service.policyDefinitions;
for (const name in definitions) {
const value = service.getPolicyValue(name);
const metadata = service.getPolicyMetadata(name);
this.policyDefinitions[name] = definitions[name];
if (value !== undefined) {
updated.push(name);
this.policies.set(name, value);
if (metadata) {
this.policyMetadata.set(name, metadata);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../../..
import { DefaultConfiguration, PolicyConfiguration } from '../../../../../platform/configuration/common/configurations.js';
import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js';
import { PolicyCategory } from '../../../../../base/common/policy.js';
import { PolicySource } from '../../../../../platform/policy/common/policy.js';

const BASE_DEFAULT_ACCOUNT: IDefaultAccount = {
enterprise: false,
Expand Down Expand Up @@ -168,4 +169,61 @@ suite('AccountPolicyService', () => {
assert.strictEqual(D, false);
}
});

test('should provide metadata for account policies', async () => {
const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: false };
defaultAccountService.setDefaultAccount(defaultAccount);

await policyConfiguration.initialize();

{
const metadataB = policyService.getPolicyMetadata('PolicySettingB');
const metadataC = policyService.getPolicyMetadata('PolicySettingC');
const metadataD = policyService.getPolicyMetadata('PolicySettingD');

assert.ok(metadataB, 'Metadata should exist for PolicySettingB');
assert.strictEqual(metadataB.source, PolicySource.Account);
assert.strictEqual(metadataB.accountSessionId, 'abc123');

assert.ok(metadataC, 'Metadata should exist for PolicySettingC');
assert.strictEqual(metadataC.source, PolicySource.Account);
assert.strictEqual(metadataC.accountSessionId, 'abc123');

assert.ok(metadataD, 'Metadata should exist for PolicySettingD');
assert.strictEqual(metadataD.source, PolicySource.Account);
assert.strictEqual(metadataD.accountSessionId, 'abc123');
}

{
// Metadata should not exist for non-active policies
const metadataA = policyService.getPolicyMetadata('PolicySettingA');
assert.strictEqual(metadataA, undefined);
}
});

test('should update metadata when account changes', async () => {
const defaultAccount1 = { ...BASE_DEFAULT_ACCOUNT, sessionId: 'session1', chat_preview_features_enabled: false };
defaultAccountService.setDefaultAccount(defaultAccount1);

await policyConfiguration.initialize();

const metadataB1 = policyService.getPolicyMetadata('PolicySettingB');
assert.ok(metadataB1);
assert.strictEqual(metadataB1.accountSessionId, 'session1');

// Change account and wait for policy update event
const defaultAccount2 = { ...BASE_DEFAULT_ACCOUNT, sessionId: 'session2', chat_preview_features_enabled: false };
const changePromise = new Promise<void>(resolve => {
const listener = policyService.onDidChange(() => {
listener.dispose();
resolve();
});
});
defaultAccountService.setDefaultAccount(defaultAccount2);
await changePromise;

const metadataB2 = policyService.getPolicyMetadata('PolicySettingB');
assert.ok(metadataB2);
assert.strictEqual(metadataB2.accountSessionId, 'session2');
});
});
Loading