Skip to content

Commit 6d1a935

Browse files
Add functionality
Signed-off-by: Roman Nikitenko <rnikiten@redhat.com>
1 parent 0c47509 commit 6d1a935

6 files changed

Lines changed: 404 additions & 3 deletions

File tree

code/ALLOWED_EXTENSIONS_POLICY.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Allowed Extensions Policy
2+
3+
Che-Code supports reading an allowed extensions policy from a file on the filesystem where the server is started. This allows administrators to control which extensions can be installed and used.
4+
5+
## Usage
6+
7+
Start the server with the `--allowed-extensions-policy-file` argument:
8+
9+
```bash
10+
./code-server --allowed-extensions-policy-file /path/to/policy.json
11+
```
12+
13+
The path can be either absolute or relative to the current working directory where the server is started.
14+
15+
## Policy File Format
16+
17+
The policy file must be a valid JSON file containing a policy object with the `AllowedExtensions` key. The value should match the format of the `extensions.allowed` configuration setting.
18+
19+
### Example: Allow All Extensions
20+
21+
```json
22+
{
23+
"AllowedExtensions": {
24+
"*": true
25+
}
26+
}
27+
```
28+
29+
### Example: Allow Specific Extensions
30+
31+
```json
32+
{
33+
"AllowedExtensions": {
34+
"ms-python.python": true,
35+
"ms-vscode.vscode-typescript-next": true,
36+
"redhat.java": true
37+
}
38+
}
39+
```
40+
41+
### Example: Allow Only Stable Versions
42+
43+
```json
44+
{
45+
"AllowedExtensions": {
46+
"ms-python.python": "stable",
47+
"ms-vscode.vscode-typescript-next": true
48+
}
49+
}
50+
```
51+
52+
### Example: Allow Specific Versions
53+
54+
```json
55+
{
56+
"AllowedExtensions": {
57+
"ms-python.python": ["1.2.3", "linux-x64@1.2.4"],
58+
"ms-vscode.vscode-typescript-next": true
59+
}
60+
}
61+
```
62+
63+
### Example: Allow All Extensions from a Publisher
64+
65+
```json
66+
{
67+
"AllowedExtensions": {
68+
"ms-python": true,
69+
"ms-vscode": true
70+
}
71+
}
72+
```
73+
74+
### Example: Deny All Extensions
75+
76+
```json
77+
{
78+
"AllowedExtensions": {
79+
"*": false
80+
}
81+
}
82+
```
83+
84+
## Policy File Location
85+
86+
The policy file should be placed in a location accessible to the server process. Common locations:
87+
88+
- `/etc/che-code/policy.json` (system-wide)
89+
- `/opt/che-code/policy.json` (installation directory)
90+
- `./policy.json` (current working directory)
91+
92+
## How It Works
93+
94+
1. When the server starts, it reads the policy file specified by `--allowed-extensions-policy-file`
95+
2. The policy file is parsed and the `AllowedExtensions` policy is extracted
96+
3. The policy is applied to the `extensions.allowed` configuration setting
97+
4. The `AllowedExtensionsService` enforces the policy when extensions are installed or used
98+
5. If the policy file changes, the server will automatically reload the policy (with a 500ms delay)
99+
100+
## Notes
101+
102+
- The policy file must be valid JSON
103+
- If the file doesn't exist or cannot be read, the server will log an error but continue to start
104+
- The policy takes precedence over user settings
105+
- The policy file is watched for changes and will be reloaded automatically
106+
107+

code/src/vs/platform/extensionManagement/common/extensionManagement.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,10 @@ export type InstallOptions = {
575575
productVersion?: IProductVersion;
576576
keepExisting?: boolean;
577577
downloadExtensionsLocally?: boolean;
578+
/**
579+
* Indicates that this extension is from DEFAULT_EXTENSIONS and should bypass policy checks
580+
*/
581+
isDefault?: boolean;
578582
/**
579583
* Context passed through to InstallExtensionResult
580584
*/
@@ -709,6 +713,7 @@ export async function computeSize(location: URI, fileService: IFileService): Pro
709713
export const ExtensionsLocalizedLabel = localize2('extensions', "Extensions");
710714
export const PreferencesLocalizedLabel = localize2('preferences', 'Preferences');
711715
export const AllowedExtensionsConfigKey = 'extensions.allowed';
716+
export const BlockNonGalleryExtensionsConfigKey = 'extensions.blockNonGalleryExtensions';
712717
export const VerifyExtensionSignatureConfigKey = 'extensions.verifySignature';
713718

714719
Registry.as<IConfigurationRegistry>(Extensions.Configuration)
@@ -781,6 +786,17 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration)
781786
],
782787
}
783788
}
789+
},
790+
[BlockNonGalleryExtensionsConfigKey]: {
791+
type: 'boolean',
792+
markdownDescription: localize('extensions.blockNonGalleryExtensions', "When enabled and the allowed extensions policy is configured, blocks installation of non-gallery extensions (VSIX files and resource extensions). Only extensions from the gallery can be installed."),
793+
default: false,
794+
scope: ConfigurationScope.APPLICATION,
795+
policy: {
796+
name: 'BlockNonGalleryExtensions',
797+
minimumVersion: '1.96',
798+
description: localize('extensions.blockNonGalleryExtensions.policy', "When enabled and the allowed extensions policy is configured, blocks installation of non-gallery extensions (VSIX files and resource extensions). Only extensions from the gallery can be installed."),
799+
},
784800
}
785801
}
786802
});

code/src/vs/platform/extensionManagement/node/extensionManagementService.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
ExtensionSignatureVerificationCode,
3535
computeSize,
3636
IAllowedExtensionsService,
37+
AllowedExtensionsConfigKey,
38+
BlockNonGalleryExtensionsConfigKey,
3739
VerifyExtensionSignatureConfigKey,
3840
shouldRequireRepositorySignatureFor,
3941
} from '../common/extensionManagement.js';
@@ -144,7 +146,12 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
144146
}
145147

146148
async install(vsix: URI, options: InstallOptions = {}): Promise<ILocalExtension> {
149+
this.logService.info('========================= INSTALL === node.ExtensionManagementService ', vsix);
150+
147151
this.logService.trace('ExtensionManagementService#install', vsix.toString());
152+
this.logService.info('+++++ ExtensionManagementService#install = ILC ', vsix.path);
153+
154+
const isDefaultExtension = options.isDefault === true;
148155

149156
const { location, cleanup } = await this.downloadVsix(vsix);
150157

@@ -155,9 +162,30 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
155162
throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", extensionId, this.productService.version));
156163
}
157164

158-
const allowedToInstall = this.allowedExtensionsService.isAllowed({ id: extensionId, version: manifest.version, publisherDisplayName: undefined });
159-
if (allowedToInstall !== true) {
160-
throw new Error(nls.localize('notAllowed', "This extension cannot be installed because {0}", allowedToInstall.value));
165+
// Block VSIX installations if the policy is configured and blockNonGalleryExtensions is enabled
166+
// Skip this check for default extensions (from DEFAULT_EXTENSIONS)
167+
if (!isDefaultExtension) {
168+
const blockNonGallery = this.configurationService.getValue<boolean>(BlockNonGalleryExtensionsConfigKey);
169+
this.logService.info('+++++ ExtensionManagementService +++ blockNonGallery ', blockNonGallery);
170+
const hasPolicy = this.configurationService.inspect(AllowedExtensionsConfigKey).policy !== undefined;
171+
if (hasPolicy && blockNonGallery) {
172+
this.logService.info('+++++ ExtensionManagementService +++ hasPolicy ', hasPolicy);
173+
throw new Error(nls.localize('vsix not allowed', "VSIX files cannot be installed because only extensions from the gallery are allowed. Please install this extension from the Extensions marketplace."));
174+
} else {
175+
this.logService.info('+++++ ExtensionManagementService +++ NOT hasPolicy ', hasPolicy);
176+
}
177+
} else {
178+
this.logService.info('!!!!!!!! ExtensionManagementService#install - skipping blockNonGallery check for default extension');
179+
}
180+
181+
// Skip allowedExtensionsService check for default extensions (from DEFAULT_EXTENSIONS)
182+
if (!isDefaultExtension) {
183+
const allowedToInstall = this.allowedExtensionsService.isAllowed({ id: extensionId, version: manifest.version, publisherDisplayName: undefined });
184+
if (allowedToInstall !== true) {
185+
throw new Error(nls.localize('notAllowed', "This extension cannot be installed because {0}", allowedToInstall.value));
186+
}
187+
} else {
188+
this.logService.info('!!!!!!!! ExtensionManagementService#install - skipping allowedExtensionsService check for default extension');
161189
}
162190

163191
const results = await this.installExtensions([{ manifest, extension: location, options }]);

0 commit comments

Comments
 (0)