Skip to content
Merged
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
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,58 @@ In the example above, the validation rules applied to `example` won't be run unl

Note that when the condition is false all validation decorators are ignored, including `isDefined`.

## Whitelisting

Even if your object is an instance of a validation class it can contain additional properties that are not defined.
If you do not want to have such properties on your object, pass special flag to `validate` method:

```typescript
import {validate} from "class-validator";
// ...
validate(post, { whitelist: true });
```

This will strip all properties that don't have any decorators. If no other decorator is suitable for your property,
you can use @Allow decorator:

```typescript
import {validate, Allow, Min} from "class-validator";

export class Post {

@Allow()
title: string;

@Min(0)
views: number;

nonWhitelistedProperty: number;
}

let post = new Post();
post.title = 'Hello world!';
post.views = 420;

post.nonWhitelistedProperty = 69;
(post as any).anotherNonWhitelistedProperty = "something";

validate(post).then(errors => {
// post.nonWhitelistedProperty is not defined
// (post as any).anotherNonWhitelistedProperty is not defined
...
});
````

If you would rather to have an error thrown when any non-whitelisted properties are present, pass another flag to
`validate` method:

```typescript
import {validate} from "class-validator";
// ...
validate(post, { whitelist: true, forbidNonWhitelisted: true });
```


## Skipping missing properties

Sometimes you may want to skip validation of the properties that does not exist in the validating object. This is
Expand Down
16 changes: 16 additions & 0 deletions src/decorator/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ export function ValidateNested(validationOptions?: ValidationOptions) {
};
}

/**
* If object has both allowed and not allowed properties a validation error will be thrown.
*/
export function Allow(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
const args: ValidationMetadataArgs = {
type: ValidationTypes.WHITELIST,
target: object.constructor,
propertyName: propertyName,
validationOptions: validationOptions
};
getFromContainer(MetadataStorage).addValidationMetadata(new ValidationMetadata(args));
};
}


/**
* Objects / object arrays marked with this decorator will also be validated.
*/
Expand Down
39 changes: 38 additions & 1 deletion src/validation/ValidationExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ export class ValidationExecutor {
const targetMetadatas = this.metadataStorage.getTargetValidationMetadatas(object.constructor, targetSchema, groups);
const groupedMetadatas = this.metadataStorage.groupByPropertyName(targetMetadatas);

if (this.validatorOptions && this.validatorOptions.whitelist)
this.whitelist(object, groupedMetadatas, validationErrors);

// General validation
Object.keys(groupedMetadatas).forEach(propertyName => {
const value = (object as any)[propertyName];
const definedMetadatas = groupedMetadatas[propertyName].filter(metadata => metadata.type === ValidationTypes.IS_DEFINED);
const metadatas = groupedMetadatas[propertyName].filter(metadata => metadata.type !== ValidationTypes.IS_DEFINED);
const metadatas = groupedMetadatas[propertyName].filter(
metadata => metadata.type !== ValidationTypes.IS_DEFINED && metadata.type !== ValidationTypes.WHITELIST);
const customValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CUSTOM_VALIDATION);
const nestedValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.NESTED_VALIDATION);
const conditionalValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CONDITIONAL_VALIDATION);
Expand All @@ -73,6 +78,38 @@ export class ValidationExecutor {
});
}

whitelist(object: any,
groupedMetadatas: { [propertyName: string]: ValidationMetadata[] },
validationErrors: ValidationError[]) {
let notAllowedProperties: string[] = [];

Object.keys(object).forEach(propertyName => {
// does this property have no metadata?
if (!groupedMetadatas[propertyName] || groupedMetadatas[propertyName].length === 0)
notAllowedProperties.push(propertyName);
});

if (notAllowedProperties.length > 0) {

if (this.validatorOptions && this.validatorOptions.forbidNonWhitelisted) {

// throw errors
notAllowedProperties.forEach(property => {
validationErrors.push({
target: object, property, value: (object as any)[property], children: undefined,
constraints: { [ValidationTypes.WHITELIST]: `property ${property} should not exist` }
});
});

} else {

// strip non allowed properties
notAllowedProperties.forEach(property => delete (object as any)[property]);

}
}
}

stripEmptyErrors(errors: ValidationError[]) {
return errors.filter(error => {
if (error.children) {
Expand Down
1 change: 1 addition & 0 deletions src/validation/ValidationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class ValidationTypes {
static CUSTOM_VALIDATION = "customValidation";
static NESTED_VALIDATION = "nestedValidation";
static CONDITIONAL_VALIDATION = "conditionalValidation";
static WHITELIST = "whitelistValidation";

/* common checkers */
static IS_DEFINED = "isDefined";
Expand Down
12 changes: 12 additions & 0 deletions src/validation/ValidatorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ export interface ValidatorOptions {
*/
skipMissingProperties?: boolean;

/**
* If set to true validator will strip validated object of any properties that do not have any decorators.
*
* Tip: if no other decorator is suitable for your property use @Allow decorator.
*/
whitelist?: false;

/**
* If set to true, instead of stripping non-whitelisted properties validator will throw an error
*/
forbidNonWhitelisted?: false;

/**
* Groups to be used during validation of the object.
*/
Expand Down
78 changes: 78 additions & 0 deletions test/functional/whitelist-validation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import "es6-shim";
import {Allow, IsDefined, Min, ValidateNested} from "../../src/decorator/decorators";
import {Validator} from "../../src/validation/Validator";
import {expect} from "chai";
import {ValidationTypes} from "../../src/validation/ValidationTypes";

// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------

const validator = new Validator();

// -------------------------------------------------------------------------
// Specifications: allowed validation
// -------------------------------------------------------------------------

describe("whitelist validation", function () {

it("should strip non whitelisted properties, but leave whitelisted untouched", function () {

class MyClass {
@IsDefined()
title: string;

@Min(0)
views: number;
}

const model: any = new MyClass();

model.title = "hello";
model.views = 56;
model.unallowedProperty = 42;
return validator.validate(model, { whitelist: true }).then(errors => {
expect(errors.length).to.be.equal(0);
expect(model.unallowedProperty).be.undefined;
expect(model.title).to.equal("hello");
expect(model.views).to.be.equal(56);
});
});

it("should be able to whitelist with @Allow", function () {

class MyClass {
@Allow()
views: number;
}

const model: any = new MyClass();

model.views = 420;
model.unallowedProperty = "non-whitelisted";

return validator.validate(model, { whitelist: true }).then(errors => {
expect(errors.length).to.be.equal(0);
expect(model.unallowedProperty).be.undefined;
expect(model.views).to.be.equal(420);
});
});

it("should throw an error when forbidNonWhitelisted flag is set", function () {

class MyClass {
}

const model: any = new MyClass();

model.unallowedProperty = "non-whitelisted";

return validator.validate(model, { whitelist: true, forbidNonWhitelisted: true }).then(errors => {
expect(errors.length).to.be.equal(1);
errors[0].target.should.be.equal(model);
errors[0].property.should.be.equal("unallowedProperty");
errors[0].constraints.should.haveOwnProperty(ValidationTypes.WHITELIST);
});
});

});