Skip to content

Commit 5e89620

Browse files
author
Mark Kuhn
authored
Add validation for dimension values (#131)
* add: throw error when invalid dimensions * move validation to separate module
1 parent d011f9d commit 5e89620

File tree

9 files changed

+172
-36
lines changed

9 files changed

+172
-36
lines changed

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@types/faker": "^4.1.5",
4040
"@types/jest": "^26.0.22",
4141
"@types/node": "^12.0.8",
42+
"@types/validator": "^13.7.5",
4243
"@typescript-eslint/eslint-plugin": "^2.23.0",
4344
"@typescript-eslint/parser": "^2.23.0",
4445
"aws-sdk": "^2.551.0",
@@ -52,6 +53,7 @@
5253
"prettier": "^1.19.1",
5354
"ts-jest": "^26.5.4",
5455
"typescript": "^3.8.0",
56+
"validator": "^13.7.0",
5557
"y18n": ">=4.0.1"
5658
},
5759
"files": [

src/Constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
*/
1515

1616
export enum Constants {
17+
MAX_DIMENSION_NAME_LENGTH = 250,
18+
MAX_DIMENSION_VALUE_LENGTH = 1024,
19+
1720
MAX_DIMENSION_SET_SIZE = 30,
1821
DEFAULT_NAMESPACE = 'aws-embedded-metrics',
1922
MAX_METRICS_PER_EVENT = 100,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class InvalidDimensionError extends Error {
2+
constructor(msg: string) {
3+
super(msg);
4+
5+
// Set the prototype explicitly.
6+
Object.setPrototypeOf(this, InvalidDimensionError.prototype);
7+
}
8+
}

src/logger/MetricsContext.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515

1616
import Configuration from '../config/Configuration';
1717
import { LOG } from '../utils/Logger';
18+
import { Validator } from '../utils/Validator';
1819
import { MetricValues } from './MetricValues';
1920
import { Unit } from './Unit';
20-
import { Constants } from '../Constants';
21-
import { DimensionSetExceededError } from '../exceptions/DimensionSetExceededError';
2221

2322
interface IProperties {
2423
[s: string]: unknown;
@@ -106,25 +105,14 @@ export class MetricsContext {
106105
this.defaultDimensions = dimensions;
107106
}
108107

109-
/**
110-
* Validates dimension set length is not more than Constants.MAX_DIMENSION_SET_SIZE
111-
*
112-
* @param dimensionSet
113-
*/
114-
public static validateDimensionSet(dimensionSet: Record<string, string>): void {
115-
if (Object.keys(dimensionSet).length > Constants.MAX_DIMENSION_SET_SIZE)
116-
throw new DimensionSetExceededError(
117-
`Maximum number of dimensions per dimension set allowed are ${Constants.MAX_DIMENSION_SET_SIZE}`)
118-
}
119-
120108
/**
121109
* Adds a new set of dimensions. Any time a new dimensions set
122110
* is added, the set is first prepended by the default dimensions.
123111
*
124112
* @param dimensions
125113
*/
126114
public putDimensions(incomingDimensionSet: Record<string, string>): void {
127-
MetricsContext.validateDimensionSet(incomingDimensionSet);
115+
Validator.validateDimensionSet(incomingDimensionSet);
128116

129117
// Duplicate dimensions sets are removed before being added to the end of the collection.
130118
// This ensures the latest dimension key-value is used as a target member on the root EMF node.
@@ -151,7 +139,7 @@ export class MetricsContext {
151139
public setDimensions(dimensionSets: Array<Record<string, string>>): void {
152140
this.shouldUseDefaultDimensions = false;
153141

154-
dimensionSets.forEach(dimensionSet => MetricsContext.validateDimensionSet(dimensionSet))
142+
dimensionSets.forEach(dimensionSet => Validator.validateDimensionSet(dimensionSet));
155143

156144
this.dimensions = dimensionSets;
157145
}

src/logger/__tests__/MetricsContext.test.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as faker from 'faker';
22
import { MetricsContext } from '../MetricsContext';
33
import { DimensionSetExceededError } from '../../exceptions/DimensionSetExceededError';
4+
import { InvalidDimensionError } from '../../exceptions/InvalidDimensionError';
45

56
test('can set property', () => {
67
// arrange
@@ -20,7 +21,7 @@ test('can set property', () => {
2021
test('setDimensions allows 30 dimensions', () => {
2122
// arrange
2223
const context = MetricsContext.empty();
23-
const numOfDimensions = 30
24+
const numOfDimensions = 30;
2425
const expectedDimensionSet = getDimensionSet(numOfDimensions);
2526

2627
// act
@@ -31,7 +32,6 @@ test('setDimensions allows 30 dimensions', () => {
3132
});
3233

3334
test('putDimension adds key to dimension and sets the dimension as a property', () => {
34-
3535
// arrange
3636
const context = MetricsContext.empty();
3737
const dimension = faker.random.word();
@@ -251,30 +251,65 @@ test('createCopyWithContext copies shouldUseDefaultDimensions', () => {
251251
test('putDimensions checks the dimension set length', () => {
252252
// arrange
253253
const context = MetricsContext.empty();
254-
const numOfDimensions = 33
254+
const numOfDimensions = 33;
255255

256256
expect(() => {
257-
context.putDimensions(getDimensionSet(numOfDimensions))
257+
context.putDimensions(getDimensionSet(numOfDimensions));
258258
}).toThrow(DimensionSetExceededError);
259259
});
260260

261261
test('setDimensions checks all the dimension sets have less than 30 dimensions', () => {
262262
// arrange
263263
const context = MetricsContext.empty();
264-
const numOfDimensions = 33
264+
const numOfDimensions = 33;
265265

266266
expect(() => {
267-
context.setDimensions([getDimensionSet(numOfDimensions)])
267+
context.setDimensions([getDimensionSet(numOfDimensions)]);
268268
}).toThrow(DimensionSetExceededError);
269269
});
270270

271+
test('adding dimensions validates them', () => {
272+
// arrange
273+
const context = MetricsContext.empty();
274+
const dimensionNameWithInvalidAscii = { '🚀': faker.random.word() };
275+
const dimensionValueWithInvalidAscii = { d1: 'مارك' };
276+
const dimensionWithLongName = { ['a'.repeat(251)]: faker.random.word() };
277+
const dimensionWithLongValue = { d1: 'a'.repeat(1025) };
278+
const dimensionWithEmptyName = { ['']: faker.random.word() };
279+
const dimensionWithEmptyValue = { d1: '' };
280+
const dimensionNameStartWithColon = { ':d1': faker.random.word() };
281+
282+
// act
283+
expect(() => {
284+
context.putDimensions(dimensionNameWithInvalidAscii);
285+
}).toThrow(InvalidDimensionError);
286+
expect(() => {
287+
context.setDimensions([dimensionValueWithInvalidAscii]);
288+
}).toThrow(InvalidDimensionError);
289+
expect(() => {
290+
context.putDimensions(dimensionWithLongName);
291+
}).toThrow(InvalidDimensionError);
292+
expect(() => {
293+
context.setDimensions([dimensionWithLongValue]);
294+
}).toThrow(InvalidDimensionError);
295+
expect(() => {
296+
context.putDimensions(dimensionWithEmptyName);
297+
}).toThrow(InvalidDimensionError);
298+
expect(() => {
299+
context.setDimensions([dimensionWithEmptyValue]);
300+
}).toThrow(InvalidDimensionError);
301+
expect(() => {
302+
context.putDimensions(dimensionNameStartWithColon);
303+
}).toThrow(InvalidDimensionError);
304+
});
305+
271306
const getDimensionSet = (numOfDimensions: number) => {
272-
const dimensionSet:Record<string, string> = {}
307+
const dimensionSet: Record<string, string> = {};
273308

274309
for (let i = 0; i < numOfDimensions; i++) {
275310
const expectedKey = `${i}`;
276311
dimensionSet[expectedKey] = faker.random.word();
277312
}
278313

279314
return dimensionSet;
280-
}
315+
};

src/serializers/LogSerializer.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,19 @@ export class LogSerializer implements ISerializer {
3838
const dimensionKeys: string[][] = [];
3939
let dimensionProperties = {};
4040

41-
context.getDimensions().forEach(d => {
42-
// we can only take the first 9 defined dimensions
43-
// the reason we do this in the serializer is because
44-
// it is possible that other sinks or formats can
45-
// support more dimensions
46-
// in the future it may make sense to introduce a higher-order
47-
// representation for sink-specific validations
48-
const keys = Object.keys(d);
41+
context.getDimensions().forEach(dimensionSet => {
42+
const keys = Object.keys(dimensionSet);
43+
4944
if (keys.length > Constants.MAX_DIMENSION_SET_SIZE) {
50-
const errMsg = `Maximum number of dimensions allowed are ${Constants.MAX_DIMENSION_SET_SIZE}.` +
51-
`Account for default dimensions if not using set_dimensions.`;
52-
throw new DimensionSetExceededError(errMsg)
45+
const errMsg =
46+
`Maximum number of dimensions allowed are ${Constants.MAX_DIMENSION_SET_SIZE}.` +
47+
`Account for default dimensions if not using set_dimensions.`;
48+
throw new DimensionSetExceededError(errMsg);
5349
}
50+
51+
5452
dimensionKeys.push(keys);
55-
dimensionProperties = { ...dimensionProperties, ...d };
53+
dimensionProperties = { ...dimensionProperties, ...dimensionSet };
5654
});
5755

5856
// eslint-disable-next-line @typescript-eslint/no-explicit-any

src/serializers/__tests__/LogSerializer.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ test('cannot serialize more than 30 dimensions', () => {
180180

181181
// assert
182182
expect(() => {
183-
serializer.serialize(context)
183+
serializer.serialize(context);
184184
}).toThrow(DimensionSetExceededError);
185185
});
186186

src/utils/Validator.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import validator from 'validator';
17+
import { Constants } from '../Constants';
18+
import { DimensionSetExceededError } from '../exceptions/DimensionSetExceededError';
19+
import { InvalidDimensionError } from '../exceptions/InvalidDimensionError';
20+
21+
export class Validator {
22+
/**
23+
* Validates dimension set.
24+
* @see [CloudWatch Dimensions](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_Dimension.html)
25+
*
26+
* @param dimensionSet
27+
* @throws {DimensionSetExceededError} Dimension set must not exceed 30 dimensions.
28+
* @throws {InvalidDimensionError} Dimension name and value must be valid.
29+
*/
30+
public static validateDimensionSet(dimensionSet: Record<string, string>): void {
31+
// Validates dimension set length
32+
if (Object.keys(dimensionSet).length > Constants.MAX_DIMENSION_SET_SIZE)
33+
throw new DimensionSetExceededError(
34+
`Maximum number of dimensions per dimension set allowed are ${Constants.MAX_DIMENSION_SET_SIZE}`,
35+
);
36+
37+
// Validate dimension key and value
38+
Object.entries(dimensionSet).forEach(([key, value]) => {
39+
dimensionSet[key] = value = String(value);
40+
41+
if (!validator.isAscii(key)) {
42+
throw new InvalidDimensionError(`Dimension key ${key} has invalid characters`);
43+
}
44+
if (!validator.isAscii(value)) {
45+
throw new InvalidDimensionError(`Dimension value ${value} has invalid characters`);
46+
}
47+
48+
if (key.trim().length == 0) {
49+
throw new InvalidDimensionError(`Dimension key ${key} must include at least one non-whitespace character`);
50+
}
51+
52+
if (value.trim().length == 0) {
53+
throw new InvalidDimensionError(`Dimension value ${value} must include at least one non-whitespace character`);
54+
}
55+
56+
if (key.length > Constants.MAX_DIMENSION_NAME_LENGTH) {
57+
throw new InvalidDimensionError(
58+
`Dimension key ${key} must not exceed maximum length ${Constants.MAX_DIMENSION_NAME_LENGTH}`,
59+
);
60+
}
61+
62+
if (value.length > Constants.MAX_DIMENSION_VALUE_LENGTH) {
63+
throw new InvalidDimensionError(
64+
`Dimension value ${value} must not exceed maximum length ${Constants.MAX_DIMENSION_VALUE_LENGTH}`,
65+
);
66+
}
67+
68+
if (key.startsWith(':')) {
69+
throw new InvalidDimensionError(`Dimension key ${key} cannot start with ':'`);
70+
}
71+
});
72+
}
73+
}

0 commit comments

Comments
 (0)