Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: disallow invalid tag values #7268

101 changes: 101 additions & 0 deletions src/lib/openapi/spec/tag-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { TAG_MAX_LENGTH, TAG_MIN_LENGTH } from '../../util';
import { validateSchema } from '../validate';

describe('tag value validation', () => {
test.each([
['minimum', TAG_MIN_LENGTH],
['maximum', TAG_MAX_LENGTH],
])(`names with the %s length are valid`, (_, length) => {
const data = {
value: 'a'.repeat(length),
type: 'simple',
};

const validationResult = validateSchema(
'#/components/schemas/tagSchema',
data,
);

expect(validationResult).toBeUndefined();
});

test(`names can not be only whitespace`, () => {
const space = ' '.repeat(TAG_MIN_LENGTH);
const data = {
value: space,
type: 'simple',
};

const validationResult = validateSchema(
'#/components/schemas/tagSchema',
data,
);

expect(validationResult).toMatchObject({
errors: [{ keyword: 'pattern', instancePath: '/value' }],
});
});

test(`names must be at least ${TAG_MIN_LENGTH} characters long, not counting leading and trailing whitespace`, () => {
const space = ' '.repeat(TAG_MIN_LENGTH);
const data = {
value: space + 'a'.repeat(TAG_MIN_LENGTH - 1) + space,
type: 'simple',
};

const validationResult = validateSchema(
'#/components/schemas/tagSchema',
data,
);

expect(validationResult).toMatchObject({
errors: [{ keyword: 'pattern', instancePath: '/value' }],
});
});

test(`spaces within a tag value counts towards its maximum length`, () => {
const space = ' '.repeat(TAG_MAX_LENGTH);
const data = {
value: `a${space}z`,
type: 'simple',
};

const validationResult = validateSchema(
'#/components/schemas/tagSchema',
data,
);

expect(validationResult).toMatchObject({
errors: [{ keyword: 'pattern', instancePath: '/value' }],
});
});

test(`leading and trailing whitespace does not count towards a name's maximum length`, () => {
const space = ' '.repeat(TAG_MAX_LENGTH);
const data = {
value: space + 'a'.repeat(TAG_MAX_LENGTH) + space,
type: 'simple',
};

const validationResult = validateSchema(
'#/components/schemas/tagSchema',
data,
);

expect(validationResult).toBeUndefined();
});

test(`tag names can contain spaces`, () => {
const data = {
value: 'tag name with spaces',
type: 'simple',
};

const validationResult = validateSchema(
'#/components/schemas/tagSchema',
data,
);

expect(validationResult).toBeUndefined();
});
});
10 changes: 5 additions & 5 deletions src/lib/openapi/spec/tag-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FromSchema } from 'json-schema-to-ts';
import { TAG_MAX_LENGTH, TAG_MIN_LENGTH } from '../../services/tag-schema';
import { TAG_MAX_LENGTH, TAG_MIN_LENGTH } from '../../util';

export const tagSchema = {
$id: '#/components/schemas/tagSchema',
Expand All @@ -11,16 +11,16 @@ export const tagSchema = {
properties: {
value: {
type: 'string',
minLength: TAG_MIN_LENGTH,
maxLength: TAG_MAX_LENGTH,
description: 'The value of the tag',
pattern: `^\\s*\\S.{${TAG_MIN_LENGTH - 2},${
thomasheartman marked this conversation as resolved.
Show resolved Hide resolved
TAG_MAX_LENGTH - 2
}}\\S\\s*$`,
description: `The value of the tag. The value must be between ${TAG_MIN_LENGTH} and ${TAG_MAX_LENGTH} characters long. Leading and trailing whitespace is ignored and will be trimmed before saving the tag value.`,
example: 'a-tag-value',
},
type: {
type: 'string',
minLength: TAG_MIN_LENGTH,
maxLength: TAG_MAX_LENGTH,
default: 'simple',
description:
'The [type](https://docs.getunleash.io/reference/tags#tag-types) of the tag',
example: 'simple',
Expand Down
3 changes: 1 addition & 2 deletions src/lib/services/tag-schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Joi from 'joi';

import { customJoi } from '../routes/util';
import { TAG_MAX_LENGTH, TAG_MIN_LENGTH } from '../util';

export const TAG_MIN_LENGTH = 2;
export const TAG_MAX_LENGTH = 50;
export const tagSchema = Joi.object()
.keys({
value: Joi.string().min(TAG_MIN_LENGTH).max(TAG_MAX_LENGTH),
Expand Down
29 changes: 29 additions & 0 deletions src/lib/services/tag-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { IUnleashConfig } from '../types/option';
import { createTestConfig } from '../../test/config/test-config';
import type EventService from '../features/events/event-service';
import FakeTagStore from '../../test/fixtures/fake-tag-store';
import TagService from './tag-service';

const config: IUnleashConfig = createTestConfig();

test('should trim tag names before saving them', async () => {
thomasheartman marked this conversation as resolved.
Show resolved Hide resolved
const tagStore = new FakeTagStore();
const service = new TagService({ tagStore }, config, {
storeEvent: async () => {},
} as unknown as EventService);

await service.createTag(
{
value: ' test ',
type: 'simple',
},
{ id: 1, username: 'audit user', ip: '' },
);

expect(tagStore.tags).toMatchObject([
{
value: 'test',
type: 'simple',
},
]);
});
6 changes: 5 additions & 1 deletion src/lib/services/tag-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ export default class TagService {
}

async createTag(tag: ITag, auditUser: IAuditUser): Promise<ITag> {
const data = await this.validate(tag);
const trimmedTag = {
...tag,
value: tag.value.trim(),
};
const data = await this.validate(trimmedTag);
await this.tagStore.createTag(data);
await this.eventService.storeEvent(
new TagCreatedEvent({
Expand Down
3 changes: 3 additions & 0 deletions src/lib/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const PREDEFINED_ROLE_TYPES = [ROOT_ROLE_TYPE, PROJECT_ROLE_TYPE];
export const ROOT_ROLE_TYPES = [ROOT_ROLE_TYPE, CUSTOM_ROOT_ROLE_TYPE];
export const PROJECT_ROLE_TYPES = [PROJECT_ROLE_TYPE, CUSTOM_PROJECT_ROLE_TYPE];

export const TAG_MIN_LENGTH = 2;
export const TAG_MAX_LENGTH = 50;

/* CONTEXT FIELD OPERATORS */

export const NOT_IN = 'NOT_IN';
Expand Down
Loading