Skip to content

Commit 8b65abe

Browse files
t-bowersoxclydin
authored andcommitted
fix(@angular/cli): improve global schema validation
- Prevent additional properties being set in cli subproperties (i.e. cli.warnings.zzzz). - Create cliGlobalOptions definition and reference in the global.cli schema. - Use global.cli schema to validate changes made with --global flag. - Add test coverage for validating global/local-only changes. - Add test coverage for setting invalid properties and sub-properties.
1 parent aad584d commit 8b65abe

File tree

5 files changed

+154
-32
lines changed

5 files changed

+154
-32
lines changed

packages/angular/cli/lib/config/workspace-schema.json

+73-5
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"description": "Show a warning when the global version is newer than the local one.",
6868
"type": "boolean"
6969
}
70-
}
70+
},
71+
"additionalProperties": false
7172
},
7273
"analytics": {
7374
"type": ["boolean", "string"],
@@ -86,7 +87,8 @@
8687
"type": "string",
8788
"format": "uuid"
8889
}
89-
}
90+
},
91+
"additionalProperties": false
9092
},
9193
"cache": {
9294
"description": "Control disk cache.",
@@ -105,7 +107,74 @@
105107
"description": "Cache base path.",
106108
"type": "string"
107109
}
110+
},
111+
"additionalProperties": false
112+
}
113+
},
114+
"additionalProperties": false
115+
},
116+
"cliGlobalOptions": {
117+
"type": "object",
118+
"properties": {
119+
"defaultCollection": {
120+
"description": "The default schematics collection to use.",
121+
"type": "string",
122+
"x-deprecated": "Use 'schematicCollections' instead."
123+
},
124+
"schematicCollections": {
125+
"type": "array",
126+
"description": "The list of schematic collections to use.",
127+
"items": {
128+
"type": "string",
129+
"uniqueItems": true
108130
}
131+
},
132+
"packageManager": {
133+
"description": "Specify which package manager tool to use.",
134+
"type": "string",
135+
"enum": ["npm", "cnpm", "yarn", "pnpm"]
136+
},
137+
"warnings": {
138+
"description": "Control CLI specific console warnings",
139+
"type": "object",
140+
"properties": {
141+
"versionMismatch": {
142+
"description": "Show a warning when the global version is newer than the local one.",
143+
"type": "boolean"
144+
}
145+
},
146+
"additionalProperties": false
147+
},
148+
"analytics": {
149+
"type": ["boolean", "string"],
150+
"description": "Share anonymous usage data with the Angular Team at Google."
151+
},
152+
"analyticsSharing": {
153+
"type": "object",
154+
"properties": {
155+
"tracking": {
156+
"description": "Analytics sharing info tracking ID.",
157+
"type": "string",
158+
"pattern": "^(GA|UA)?-\\d+-\\d+$"
159+
},
160+
"uuid": {
161+
"description": "Analytics sharing info universally unique identifier.",
162+
"type": "string",
163+
"format": "uuid"
164+
}
165+
},
166+
"additionalProperties": false
167+
},
168+
"completion": {
169+
"type": "object",
170+
"description": "Angular CLI completion settings.",
171+
"properties": {
172+
"prompted": {
173+
"type": "boolean",
174+
"description": "Whether the user has been prompted to add completion command prompt."
175+
}
176+
},
177+
"additionalProperties": false
109178
}
110179
},
111180
"additionalProperties": false
@@ -575,14 +644,13 @@
575644
"type": "object",
576645
"properties": {
577646
"$schema": {
578-
"type": "string",
579-
"format": "uri"
647+
"type": "string"
580648
},
581649
"version": {
582650
"$ref": "#/definitions/fileVersion"
583651
},
584652
"cli": {
585-
"$ref": "#/definitions/cliOptions"
653+
"$ref": "#/definitions/cliGlobalOptions"
586654
},
587655
"schematics": {
588656
"$ref": "#/definitions/schematicOptions"

packages/angular/cli/src/commands/config/cli.ts

+1-22
Original file line numberDiff line numberDiff line change
@@ -97,27 +97,6 @@ export class ConfigCommandModule
9797
throw new CommandModuleError('Invalid Path.');
9898
}
9999

100-
const validGlobalCliPaths = new Set<string>([
101-
'cli.warnings.versionMismatch',
102-
'cli.defaultCollection',
103-
'cli.schematicCollections',
104-
'cli.packageManager',
105-
106-
'cli.analytics',
107-
'cli.analyticsSharing.tracking',
108-
'cli.analyticsSharing.uuid',
109-
110-
'cli.completion.prompted',
111-
]);
112-
113-
if (
114-
options.global &&
115-
!options.jsonPath.startsWith('schematics.') &&
116-
!validGlobalCliPaths.has(options.jsonPath)
117-
) {
118-
throw new CommandModuleError('Invalid Path.');
119-
}
120-
121100
const [config, configPath] = await getWorkspaceRaw(options.global ? 'global' : 'local');
122101
const { logger } = this.context;
123102

@@ -140,7 +119,7 @@ export class ConfigCommandModule
140119
return 1;
141120
}
142121

143-
await validateWorkspace(parseJson(config.content));
122+
await validateWorkspace(parseJson(config.content), options.global ?? false);
144123

145124
config.save();
146125

packages/angular/cli/src/utilities/config.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,20 @@ export async function getWorkspaceRaw(
240240
return [new JSONFile(configPath), configPath];
241241
}
242242

243-
export async function validateWorkspace(data: json.JsonObject): Promise<void> {
244-
const schema = readAndParseJson(workspaceSchemaPath) as json.schema.JsonSchema;
243+
export async function validateWorkspace(data: json.JsonObject, isGlobal: boolean): Promise<void> {
244+
const schema = readAndParseJson(workspaceSchemaPath);
245+
246+
// We should eventually have a dedicated global config schema and use that to validate.
247+
const schemaToValidate: json.schema.JsonSchema = isGlobal
248+
? {
249+
'$ref': '#/definitions/global',
250+
definitions: schema['definitions'],
251+
}
252+
: schema;
253+
245254
const { formats } = await import('@angular-devkit/schematics');
246255
const registry = new json.schema.CoreSchemaRegistry(formats.standardFormats);
247-
const validator = await registry.compile(schema).toPromise();
256+
const validator = await registry.compile(schemaToValidate).toPromise();
248257

249258
const { success, errors } = await validator(data).toPromise();
250259
if (!success) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { homedir } from 'os';
2+
import * as path from 'path';
3+
import { deleteFile, expectFileToExist } from '../../../utils/fs';
4+
import { ng, silentNg } from '../../../utils/process';
5+
import { expectToFail } from '../../../utils/utils';
6+
7+
export default async function () {
8+
let ngError: Error;
9+
10+
ngError = await expectToFail(() => silentNg('config', 'cli.completion.prompted', 'true'));
11+
12+
if (
13+
!ngError.message.includes('Data path "/cli" must NOT have additional properties(completion).')
14+
) {
15+
throw new Error('Should have failed with must NOT have additional properties(completion).');
16+
}
17+
18+
ngError = await expectToFail(() =>
19+
silentNg('config', '--global', 'cli.completion.invalid', 'true'),
20+
);
21+
22+
if (
23+
!ngError.message.includes(
24+
'Data path "/cli/completion" must NOT have additional properties(invalid).',
25+
)
26+
) {
27+
throw new Error('Should have failed with must NOT have additional properties(invalid).');
28+
}
29+
30+
ngError = await expectToFail(() => silentNg('config', '--global', 'cli.cache.enabled', 'true'));
31+
32+
if (!ngError.message.includes('Data path "/cli" must NOT have additional properties(cache).')) {
33+
throw new Error('Should have failed with must NOT have additional properties(cache).');
34+
}
35+
36+
ngError = await expectToFail(() => silentNg('config', 'cli.completion.prompted'));
37+
38+
if (!ngError.message.includes('Value cannot be found.')) {
39+
throw new Error('Should have failed with Value cannot be found.');
40+
}
41+
42+
await ng('config', '--global', 'cli.completion.prompted', 'true');
43+
const { stdout } = await silentNg('config', '--global', 'cli.completion.prompted');
44+
45+
if (!stdout.includes('true')) {
46+
throw new Error(`Expected "true", received "${JSON.stringify(stdout)}".`);
47+
}
48+
49+
await expectFileToExist(path.join(homedir(), '.angular-config.json'));
50+
await deleteFile(path.join(homedir(), '.angular-config.json'));
51+
}

tests/legacy-cli/e2e/tests/commands/config/config-set.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1-
import { ng } from '../../../utils/process';
1+
import { ng, silentNg } from '../../../utils/process';
22
import { expectToFail } from '../../../utils/utils';
33

44
export default async function () {
5-
await expectToFail(() => ng('config', 'cli.warnings.zzzz'));
5+
let ngError: Error;
6+
7+
ngError = await expectToFail(() => silentNg('config', 'cli.warnings.zzzz', 'true'));
8+
if (
9+
!ngError.message.includes(
10+
'Data path "/cli/warnings" must NOT have additional properties(zzzz).',
11+
)
12+
) {
13+
throw new Error('Should have failed with must NOT have additional properties(zzzz).');
14+
}
15+
16+
ngError = await expectToFail(() => silentNg('config', 'cli.warnings.zzzz'));
17+
if (!ngError.message.includes('Value cannot be found.')) {
18+
throw new Error('Should have failed with Value cannot be found.');
19+
}
20+
621
await ng('config', 'cli.warnings.versionMismatch', 'false');
722
const { stdout } = await ng('config', 'cli.warnings.versionMismatch');
823
if (!stdout.includes('false')) {

0 commit comments

Comments
 (0)