mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Authz] Eslint Rule for Security Config (#193187)
## Summary ESLint rule is introduced to enforce the migration of access tags in route configurations to the `security.authz.requiredPrivileges` field. It ensures that security configurations are correctly applied in both standard and versioned routes. Will be enabled after https://github.com/elastic/kibana/pull/191973 is merged. The rule covers: - **Access Tag Migration.** Moves `access:<privilege>` tags from the `options.tags` property to `security.authz.requiredPrivileges`. Preserves any non-access tags in the tags property. - **Missing Security Config Detection.** Reports an error if no security config is found in the route or version. Suggests adding a default security configuration `authz: { enabled: false }`. ### Note There is an indentation issues with the test, `dedent` doesn't solve most of the issues and since `RuleTester` was designed to test a single rule at a time,I couldn't enable multiple fixes (including indent ones) before checking output. Manually adjusted the indentation. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios __Fixes: https://github.com/elastic/kibana/issues/191715__ __Related: https://github.com/elastic/kibana/issues/191710__ --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
12ea569eb9
commit
8776fe588d
2 changed files with 703 additions and 0 deletions
|
@ -0,0 +1,318 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
const routeMethods = ['get', 'put', 'delete', 'post'];
|
||||
const ACCESS_TAG_PREFIX = 'access:';
|
||||
|
||||
const isStringLiteral = (el) => el.type === 'Literal' && typeof el.value === 'string';
|
||||
const isLiteralAccessTag = (el) => isStringLiteral(el) && el.value.startsWith(ACCESS_TAG_PREFIX);
|
||||
const isLiteralNonAccessTag = (el) =>
|
||||
isStringLiteral(el) && !el.value.startsWith(ACCESS_TAG_PREFIX);
|
||||
|
||||
const isTemplateLiteralAccessTag = (el) =>
|
||||
el.type === 'TemplateLiteral' && el.quasis[0].value.raw.startsWith(ACCESS_TAG_PREFIX);
|
||||
const isTemplateLiteralNonAccessTag = (el) =>
|
||||
el.type === 'TemplateLiteral' && !el.quasis[0].value.raw.startsWith(ACCESS_TAG_PREFIX);
|
||||
|
||||
const maybeReportDisabledSecurityConfig = (node, context, isVersionedRoute = false) => {
|
||||
const callee = node.callee;
|
||||
const isAddVersionCall =
|
||||
callee.type === 'MemberExpression' && callee.property.name === 'addVersion';
|
||||
|
||||
const disabledSecurityConfig = `
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason: 'This route is opted out from authorization',
|
||||
},
|
||||
},`;
|
||||
|
||||
// Skipping root route call intentionally, we will check root route security config in addVersion node traversal
|
||||
if (isVersionedRoute && !isAddVersionCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVersionedRoute) {
|
||||
const [versionConfig] = node.arguments;
|
||||
|
||||
if (versionConfig && versionConfig.type === 'ObjectExpression') {
|
||||
const securityInVersion = versionConfig.properties.find(
|
||||
(property) => property.key && property.key.name === 'security'
|
||||
);
|
||||
|
||||
if (securityInVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentNode = node;
|
||||
|
||||
const hasSecurityInRoot = (config) => {
|
||||
const securityInRoot = config.properties.find(
|
||||
(property) => property.key && property.key.name === 'security'
|
||||
);
|
||||
|
||||
if (securityInRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const optionsProperty = config.properties.find(
|
||||
(prop) => prop.key && prop.key.name === 'options'
|
||||
);
|
||||
|
||||
if (optionsProperty?.value?.properties) {
|
||||
const tagsProperty = optionsProperty.value.properties.find(
|
||||
(prop) => prop.key.name === 'tags'
|
||||
);
|
||||
|
||||
const accessTagsFilter = (el) => isLiteralAccessTag(el) || isTemplateLiteralAccessTag(el);
|
||||
const accessTags = tagsProperty.value.elements.filter(accessTagsFilter);
|
||||
|
||||
return accessTags.length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
while (
|
||||
currentNode &&
|
||||
currentNode.type === 'CallExpression' &&
|
||||
currentNode.callee.type === 'MemberExpression'
|
||||
) {
|
||||
const callee = currentNode.callee;
|
||||
|
||||
if (
|
||||
callee.object &&
|
||||
callee.object.property &&
|
||||
callee.object.property.name === 'versioned' &&
|
||||
routeMethods.includes(callee.property.name)
|
||||
) {
|
||||
const [routeConfig] = currentNode.arguments;
|
||||
|
||||
if (routeConfig && routeConfig.type === 'ObjectExpression') {
|
||||
const securityInRoot = hasSecurityInRoot(routeConfig);
|
||||
|
||||
// If security is missing in both the root and the version
|
||||
if (!securityInRoot) {
|
||||
context.report({
|
||||
node: versionConfig,
|
||||
message: 'Security config is missing in addVersion call',
|
||||
fix(fixer) {
|
||||
const versionProperty = versionConfig.properties.find(
|
||||
(property) => property.key && property.key.name === 'version'
|
||||
);
|
||||
const insertPosition = versionProperty.range[1];
|
||||
|
||||
return fixer.insertTextAfterRange(
|
||||
[insertPosition, insertPosition + 1],
|
||||
`${disabledSecurityConfig}`
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
currentNode = callee.object;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const [routeConfig] = node.arguments;
|
||||
const securityProperty = routeConfig.properties.find(
|
||||
(property) => property.key && property.key.name === 'security'
|
||||
);
|
||||
|
||||
if (!securityProperty) {
|
||||
const pathProperty = routeConfig.properties.find((prop) => prop.key.name === 'path');
|
||||
context.report({
|
||||
node: routeConfig,
|
||||
message: 'Security config is missing',
|
||||
fix(fixer) {
|
||||
const insertPosition = pathProperty.range[1];
|
||||
|
||||
return fixer.insertTextAfterRange(
|
||||
[insertPosition, insertPosition + 1],
|
||||
`${disabledSecurityConfig}`
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRouteConfig = (node, context, isVersionedRoute = false) => {
|
||||
const [routeConfig] = node.arguments;
|
||||
|
||||
if (routeConfig && routeConfig.type === 'ObjectExpression') {
|
||||
const optionsProperty = routeConfig.properties.find(
|
||||
(prop) => prop.key && prop.key.name === 'options'
|
||||
);
|
||||
|
||||
if (!optionsProperty) {
|
||||
return maybeReportDisabledSecurityConfig(node, context, isVersionedRoute);
|
||||
}
|
||||
|
||||
if (optionsProperty?.value?.properties) {
|
||||
const tagsProperty = optionsProperty.value.properties.find(
|
||||
(prop) => prop.key.name === 'tags'
|
||||
);
|
||||
|
||||
const accessTagsFilter = (el) => isLiteralAccessTag(el) || isTemplateLiteralAccessTag(el);
|
||||
const nonAccessTagsFilter = (el) =>
|
||||
isLiteralNonAccessTag(el) || isTemplateLiteralNonAccessTag(el);
|
||||
|
||||
const getAccessPrivilege = (el) => {
|
||||
if (el.type === 'Literal') {
|
||||
return `'${el.value.split(':')[1]}'`;
|
||||
}
|
||||
|
||||
if (el.type === 'TemplateLiteral') {
|
||||
const firstQuasi = el.quasis[0].value.raw;
|
||||
|
||||
if (firstQuasi.startsWith(ACCESS_TAG_PREFIX)) {
|
||||
const staticPart = firstQuasi.split(ACCESS_TAG_PREFIX)[1] || '';
|
||||
|
||||
const dynamicParts = el.expressions.map((expression, index) => {
|
||||
const dynamicPlaceholder = `\${${expression.name}}`;
|
||||
const nextQuasi = el.quasis[index + 1].value.raw || '';
|
||||
return `${dynamicPlaceholder}${nextQuasi}`;
|
||||
});
|
||||
|
||||
return `\`${staticPart}${dynamicParts.join('')}\``;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!tagsProperty) {
|
||||
return maybeReportDisabledSecurityConfig(node, context, isVersionedRoute);
|
||||
}
|
||||
|
||||
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
|
||||
const accessTags = tagsProperty.value.elements.filter(accessTagsFilter);
|
||||
const nonAccessTags = tagsProperty.value.elements.filter(nonAccessTagsFilter);
|
||||
|
||||
if (!accessTags.length) {
|
||||
return maybeReportDisabledSecurityConfig(node, context, isVersionedRoute);
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: tagsProperty,
|
||||
message: `Move 'access' tags to security.authz.requiredPrivileges.`,
|
||||
fix(fixer) {
|
||||
const accessPrivileges = accessTags.map(getAccessPrivilege);
|
||||
|
||||
const securityConfig = `security: {
|
||||
authz: {
|
||||
requiredPrivileges: [${accessPrivileges.map((priv) => priv).join(', ')}],
|
||||
},
|
||||
}`;
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
const fixes = [];
|
||||
let remainingOptions = [];
|
||||
|
||||
// If there are non-access tags, keep the 'tags' property with those
|
||||
if (nonAccessTags.length > 0) {
|
||||
const nonAccessTagsText = `[${nonAccessTags
|
||||
.map((tag) => sourceCode.getText(tag))
|
||||
.join(', ')}]`;
|
||||
fixes.push(fixer.replaceText(tagsProperty.value, nonAccessTagsText));
|
||||
} else {
|
||||
// Check if 'options' will be empty after removing 'tags'
|
||||
remainingOptions = optionsProperty.value.properties.filter(
|
||||
(prop) => prop.key.name !== 'tags'
|
||||
);
|
||||
|
||||
// If options are empty, replace the entire 'options' with 'security' config
|
||||
if (remainingOptions.length === 0) {
|
||||
fixes.push(fixer.replaceText(optionsProperty, securityConfig));
|
||||
}
|
||||
}
|
||||
|
||||
// If 'options' was replaced or has other properties, insert security separately
|
||||
if (remainingOptions.length > 0) {
|
||||
// If no non-access tags, remove 'tags'
|
||||
const nextToken = sourceCode.getTokenAfter(tagsProperty);
|
||||
|
||||
if (nextToken && nextToken.value === ',') {
|
||||
// Remove the 'tags' property and the trailing comma
|
||||
fixes.push(fixer.removeRange([tagsProperty.range[0], nextToken.range[1]]));
|
||||
} else {
|
||||
fixes.push(fixer.remove(tagsProperty));
|
||||
}
|
||||
fixes.push(fixer.insertTextBefore(optionsProperty, `${securityConfig},`));
|
||||
}
|
||||
|
||||
if (nonAccessTags.length && !remainingOptions.length) {
|
||||
fixes.push(fixer.insertTextBefore(optionsProperty, `${securityConfig},`));
|
||||
}
|
||||
|
||||
return fixes;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ESLint Rule: Migrate `access` tags in route configurations to `security.authz.requiredPrivileges`.
|
||||
*
|
||||
* This rule checks for the following in route configurations:
|
||||
* 1. If a route (e.g., `router.get()`, `router.post()`) contains an `options` property with `tags`.
|
||||
* 2. If `tags` contains any `access:<privilege>` tags, these are moved to `security.authz.requiredPrivileges`.
|
||||
* 3. If no `security` configuration exists, it reports an error and suggests adding a default `security` config.
|
||||
* 4. It handles both standard routes and versioned routes (e.g., `router.versioned.post()`, `router.addVersion()`).
|
||||
* 5. If other non-access tags exist, they remain in `tags`.
|
||||
*/
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Migrate routes with and without access tags to security config',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
},
|
||||
fixable: 'code',
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.object &&
|
||||
callee.object.name === 'router' &&
|
||||
routeMethods.includes(callee.property.name)
|
||||
) {
|
||||
handleRouteConfig(node, context, false);
|
||||
}
|
||||
|
||||
if (
|
||||
(callee.type === 'MemberExpression' && callee.property.name === 'addVersion') ||
|
||||
(callee.object &&
|
||||
callee.object.type === 'MemberExpression' &&
|
||||
callee.object.object.name === 'router' &&
|
||||
callee.object.property.name === 'versioned' &&
|
||||
routeMethods.includes(callee.property.name))
|
||||
) {
|
||||
const versionConfig = node.arguments[0];
|
||||
|
||||
if (versionConfig && versionConfig.type === 'ObjectExpression') {
|
||||
handleRouteConfig(node, context, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
const { RuleTester } = require('eslint');
|
||||
const rule = require('./no_deprecated_authz_config');
|
||||
const dedent = require('dedent');
|
||||
|
||||
// Indentation is a big problem in the test cases, dedent library does not work as expected.
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2018,
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('no_deprecated_authz_config', rule, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
router.get(
|
||||
{
|
||||
path: '/api/security/authz_poc/simple_privileges_example_1',
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason: 'This route is opted out from authorization ',
|
||||
},
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`,
|
||||
name: 'valid: security config is present and authz is disabled',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['otherTag'],
|
||||
},
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['somePrivilege'],
|
||||
},
|
||||
},
|
||||
});
|
||||
`,
|
||||
name: 'valid: security config is present and authz is enabled',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
router.versioned
|
||||
.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['otherTag'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['managePrivileges'],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`,
|
||||
name: 'valid: security config is present for versioned route',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
router.versioned
|
||||
.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['otherTag'],
|
||||
},
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['managePrivileges'],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`,
|
||||
name: 'valid: security config is present for versioned route provided in root route definition',
|
||||
},
|
||||
],
|
||||
|
||||
invalid: [
|
||||
{
|
||||
code: dedent(`
|
||||
router.get(
|
||||
{
|
||||
path: '/test/path',
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`),
|
||||
errors: [{ message: 'Security config is missing' }],
|
||||
output: dedent(`
|
||||
router.get(
|
||||
{
|
||||
path: '/test/path',
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason: 'This route is opted out from authorization',
|
||||
},
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`),
|
||||
name: 'invalid: security config is missing',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ message: "Move 'access' tags to security.authz.requiredPrivileges." }],
|
||||
output: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution'],
|
||||
},
|
||||
},
|
||||
});
|
||||
`,
|
||||
name: 'invalid: access tags are string literals, move to security.authz.requiredPrivileges',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: [\`access:\${APP_ID}-entity-analytics\`],
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ message: "Move 'access' tags to security.authz.requiredPrivileges." }],
|
||||
output: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [\`\${APP_ID}-entity-analytics\`],
|
||||
},
|
||||
},
|
||||
});
|
||||
`,
|
||||
name: 'invalid: access tags are template literals, move to security.authz.requiredPrivileges',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['access:securitySolution', 'otherTag'],
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ message: "Move 'access' tags to security.authz.requiredPrivileges." }],
|
||||
output: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution'],
|
||||
},
|
||||
},options: {
|
||||
tags: ['otherTag'],
|
||||
},
|
||||
});
|
||||
`,
|
||||
name: 'invalid: both access tags and non access tags, move only access tags to security.authz.requiredPrivileges',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
router.versioned
|
||||
.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [ApiActionPermission.ManageSpaces],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`,
|
||||
errors: [{ message: "Move 'access' tags to security.authz.requiredPrivileges." }],
|
||||
output: `
|
||||
router.versioned
|
||||
.get({
|
||||
path: '/some/path',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution'],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [ApiActionPermission.ManageSpaces],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`,
|
||||
name: 'invalid: versioned route root access tags, move access tags to security.authz.requiredPrivileges',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['access:securitySolution', \`access:\${APP_ID}-entity-analytics\`],
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ message: "Move 'access' tags to security.authz.requiredPrivileges." }],
|
||||
output: `
|
||||
router.get({
|
||||
path: '/some/path',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution', \`\${APP_ID}-entity-analytics\`],
|
||||
},
|
||||
},
|
||||
});
|
||||
`,
|
||||
name: 'invalid: string and template literal access tags, move both to security.authz.requiredPrivileges',
|
||||
},
|
||||
{
|
||||
code: dedent(`
|
||||
router.versioned
|
||||
.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['otherTag'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`),
|
||||
errors: [{ message: 'Security config is missing in addVersion call' }],
|
||||
output: dedent(`
|
||||
router.versioned
|
||||
.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['otherTag'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason: 'This route is opted out from authorization',
|
||||
},
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`),
|
||||
name: 'invalid: security config is missing in addVersion call',
|
||||
},
|
||||
{
|
||||
code: dedent(`
|
||||
router.versioned
|
||||
.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['otherTag'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
.addVersion(
|
||||
{
|
||||
version: '2',
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`),
|
||||
errors: [
|
||||
{ message: 'Security config is missing in addVersion call' },
|
||||
{ message: 'Security config is missing in addVersion call' },
|
||||
],
|
||||
output: dedent(`
|
||||
router.versioned
|
||||
.get({
|
||||
path: '/some/path',
|
||||
options: {
|
||||
tags: ['otherTag'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason: 'This route is opted out from authorization',
|
||||
},
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
.addVersion(
|
||||
{
|
||||
version: '2',
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason: 'This route is opted out from authorization',
|
||||
},
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
`),
|
||||
name: 'invalid: security config is missing in multiple addVersion call',
|
||||
},
|
||||
],
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue