From 39fc05c0ecc058ca1f950d159aa4f43528017fca Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Fri, 24 May 2019 12:30:24 +0100 Subject: [PATCH 01/15] added increased parsing support (no breaking changes) for the security decorator, allowing it to work with the @Security decorator as currently defined in typescript-rest --- .gitignore | 1 + src/metadata/controllerGenerator.ts | 19 +++++---- src/metadata/metadataGenerator.ts | 2 +- src/metadata/methodGenerator.ts | 62 ++++++++++++++++++----------- src/swagger/generator.ts | 2 +- src/utils/decoratorUtils.ts | 33 +++++++++++++-- 6 files changed, 83 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 84e01b0..cd2cf1b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules coverage *.log npm-debug.log* +.idea/ diff --git a/src/metadata/controllerGenerator.ts b/src/metadata/controllerGenerator.ts index 9363945..6fa4d8a 100644 --- a/src/metadata/controllerGenerator.ts +++ b/src/metadata/controllerGenerator.ts @@ -2,7 +2,7 @@ import * as ts from 'typescript'; import { Controller } from './metadataGenerator'; import { getSuperClass } from './resolveType'; import { MethodGenerator } from './methodGenerator'; -import { getDecorators, getDecoratorTextValue } from '../utils/decoratorUtils'; +import {getDecorators, getDecoratorTextValue, parseSecurityDecoratorArguments} from '../utils/decoratorUtils'; import {normalizePath} from '../utils/pathUtils'; import * as _ from 'lodash'; @@ -79,15 +79,18 @@ export class ControllerGenerator { } private getMethodSecurity() { - if (!this.node.parent) { throw new Error('Controller node doesn\'t have a valid parent source file.'); } - if (!this.node.name) { throw new Error('Controller node doesn\'t have a valid name.'); } + if (!this.node.parent) { + throw new Error('Controller node doesn\'t have a valid parent source file.'); + } + if (!this.node.name) { + throw new Error('Controller node doesn\'t have a valid name.'); + } const securityDecorators = getDecorators(this.node, decorator => decorator.text === 'Security'); - if (!securityDecorators || !securityDecorators.length) { return undefined; } + if (!securityDecorators || !securityDecorators.length) { + return undefined; + } - return securityDecorators.map(d => ({ - name: d.arguments[0], - scopes: d.arguments[1] ? (d.arguments[1] as any).elements.map((e: any) => e.text) : undefined - })); + return securityDecorators.map(parseSecurityDecoratorArguments); } } diff --git a/src/metadata/metadataGenerator.ts b/src/metadata/metadataGenerator.ts index 4c6ddcd..1cca0d7 100644 --- a/src/metadata/metadataGenerator.ts +++ b/src/metadata/metadataGenerator.ts @@ -128,7 +128,7 @@ export interface Parameter { } export interface Security { - name: string; + name?: string; scopes?: string[]; } diff --git a/src/metadata/methodGenerator.ts b/src/metadata/methodGenerator.ts index 5930552..0379864 100644 --- a/src/metadata/methodGenerator.ts +++ b/src/metadata/methodGenerator.ts @@ -1,10 +1,10 @@ import * as ts from 'typescript'; -import { Method, ResponseData, ResponseType, Type } from './metadataGenerator'; -import { resolveType } from './resolveType'; -import { ParameterGenerator } from './parameterGenerator'; -import { getJSDocDescription, getJSDocTag, isExistJSDocTag } from '../utils/jsDocUtils'; -import { getDecorators } from '../utils/decoratorUtils'; -import { normalizePath } from '../utils/pathUtils'; +import {Method, ResponseData, ResponseType, Type} from './metadataGenerator'; +import {resolveType} from './resolveType'; +import {ParameterGenerator} from './parameterGenerator'; +import {getJSDocDescription, getJSDocTag, isExistJSDocTag} from '../utils/jsDocUtils'; +import {getDecorators, parseSecurityDecoratorArguments} from '../utils/decoratorUtils'; +import {normalizePath} from '../utils/pathUtils'; import * as pathUtil from 'path'; export class MethodGenerator { @@ -27,7 +27,9 @@ export class MethodGenerator { } public generate(): Method { - if (!this.isValid()) { throw new Error('This isn\'t a valid controller method.'); } + if (!this.isValid()) { + throw new Error('This isn\'t a valid controller method.'); + } const identifier = this.node.name as ts.Identifier; const type = resolveType(this.node.type, this.genericTypeMap); @@ -85,7 +87,9 @@ export class MethodGenerator { private processMethodDecorators() { const httpMethodDecorators = getDecorators(this.node, decorator => this.supportsPathMethod(decorator.text)); - if (!httpMethodDecorators || !httpMethodDecorators.length) { return; } + if (!httpMethodDecorators || !httpMethodDecorators.length) { + return; + } if (httpMethodDecorators.length > 1) { throw new Error(`Only one HTTP Method decorator in '${this.getCurrentLocation}' method is acceptable, Found: ${httpMethodDecorators.map(d => d.text).join(', ')}`); } @@ -108,7 +112,9 @@ export class MethodGenerator { private getMethodResponses(): ResponseType[] { const decorators = getDecorators(this.node, decorator => decorator.text === 'Response'); - if (!decorators || !decorators.length) { return []; } + if (!decorators || !decorators.length) { + return []; + } return decorators.map(decorator => { let description = ''; @@ -148,20 +154,29 @@ export class MethodGenerator { private getMethodSuccessResponseData(type: Type): ResponseData { switch (type.typeName) { - case 'void': return { status: '204', type: type }; - case 'NewResource': return { status: '201', type: type.typeArgument || type }; - case 'RequestAccepted': return { status: '202', type: type.typeArgument || type }; - case 'MovedPermanently': return { status: '301', type: type.typeArgument || type }; - case 'MovedTemporarily': return { status: '302', type: type.typeArgument || type }; + case 'void': + return {status: '204', type: type}; + case 'NewResource': + return {status: '201', type: type.typeArgument || type}; + case 'RequestAccepted': + return {status: '202', type: type.typeArgument || type}; + case 'MovedPermanently': + return {status: '301', type: type.typeArgument || type}; + case 'MovedTemporarily': + return {status: '302', type: type.typeArgument || type}; case 'DownloadResource': - case 'DownloadBinaryData': return { status: '200', type: { typeName: 'buffer' } }; - default: return { status: '200', type: type }; + case 'DownloadBinaryData': + return {status: '200', type: {typeName: 'buffer'}}; + default: + return {status: '200', type: type}; } } private getMethodSuccessExamples() { const exampleDecorators = getDecorators(this.node, decorator => decorator.text === 'Example'); - if (!exampleDecorators || !exampleDecorators.length) { return undefined; } + if (!exampleDecorators || !exampleDecorators.length) { + return undefined; + } if (exampleDecorators.length > 1) { throw new Error(`Only one Example decorator allowed in '${this.getCurrentLocation}' method.`); } @@ -207,7 +222,9 @@ export class MethodGenerator { private getDecoratorValues(decoratorName: string) { const tagsDecorators = getDecorators(this.node, decorator => decorator.text === decoratorName); - if (!tagsDecorators || !tagsDecorators.length) { return []; } + if (!tagsDecorators || !tagsDecorators.length) { + return []; + } if (tagsDecorators.length > 1) { throw new Error(`Only one ${decoratorName} decorator allowed in '${this.getCurrentLocation}' method.`); } @@ -218,12 +235,11 @@ export class MethodGenerator { private getMethodSecurity() { const securityDecorators = getDecorators(this.node, decorator => decorator.text === 'Security'); - if (!securityDecorators || !securityDecorators.length) { return undefined; } + if (!securityDecorators || !securityDecorators.length) { + return undefined; + } - return securityDecorators.map(d => ({ - name: d.arguments[0], - scopes: d.arguments[1] ? (d.arguments[1] as any).elements.map((e: any) => e.text) : undefined - })); + return securityDecorators.map(parseSecurityDecoratorArguments); } private getInitializerValue(initializer: any) { diff --git a/src/swagger/generator.ts b/src/swagger/generator.ts index 9c404b3..03b0149 100644 --- a/src/swagger/generator.ts +++ b/src/swagger/generator.ts @@ -123,7 +123,7 @@ export class SpecGenerator { if (method.tags.length) { pathMethod.tags = method.tags; } if (method.security) { pathMethod.security = method.security.map(s => ({ - [s.name]: s.scopes || [] + [s.name || 'undefined']: s.scopes || [] })); } this.handleMethodConsumes(method, pathMethod); diff --git a/src/utils/decoratorUtils.ts b/src/utils/decoratorUtils.ts index 998fc2a..6ebc034 100644 --- a/src/utils/decoratorUtils.ts +++ b/src/utils/decoratorUtils.ts @@ -1,8 +1,33 @@ import * as ts from 'typescript'; +import {Security} from '../metadata/metadataGenerator'; + +export function parseSecurityDecoratorArguments(decoratorData: DecoratorData): Security { + if (decoratorData.arguments.length === 1) { + // according to typescript-rest @Security decorator definition, when only one argument has been provided, + // scopes must be the only parameter + return {name: undefined, scopes: parseScopesArgument( decoratorData.arguments[0] )}; + } else { + // in all other cases, maintain previous functionality - assume two parameters: name, scopes + return { name: decoratorData.arguments[0], scopes: parseScopesArgument( decoratorData.arguments[1] ) }; + } + + function parseScopesArgument(arg: any): Array | undefined { + // typescript-rest @Security allows scopes to be a string or an array, so we need to support both + if (typeof arg === 'string') { + // wrap in an array for compatibility with upstream generator logic + return [arg]; + } else { + // array from metadata needs to be extracted and converted to normal string array + return arg ? (arg as any).elements.map((e: any) => e.text) : undefined; + } + } +} export function getDecorators(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean): DecoratorData[] { const decorators = node.decorators; - if (!decorators || !decorators.length) { return []; } + if (!decorators || !decorators.length) { + return []; + } return decorators .map(d => { @@ -36,7 +61,9 @@ export function getDecorators(node: ts.Node, isMatching: (identifier: DecoratorD function getDecorator(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { const decorators = getDecorators(node, isMatching); - if (!decorators || !decorators.length) { return; } + if (!decorators || !decorators.length) { + return; + } return decorators[0]; } @@ -53,7 +80,7 @@ export function getDecoratorTextValue(node: ts.Node, isMatching: (identifier: De export function getDecoratorOptions(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { const decorator = getDecorator(node, isMatching); - return decorator && typeof decorator.arguments[1] === 'object' ? decorator.arguments[1] as {[key: string]: any} : undefined; + return decorator && typeof decorator.arguments[1] === 'object' ? decorator.arguments[1] as { [key: string]: any } : undefined; } export function isDecorator(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { From 69b30d5458e9aac56526d6973c5a4e426a774fe2 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Tue, 28 May 2019 10:52:55 +0100 Subject: [PATCH 02/15] added logic to fuel the security definitions with defaults drawn from the swagger config file, when a named security parameter has not been provided --- src/swagger/generator.ts | 66 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/swagger/generator.ts b/src/swagger/generator.ts index 03b0149..953cf28 100644 --- a/src/swagger/generator.ts +++ b/src/swagger/generator.ts @@ -122,9 +122,69 @@ export class SpecGenerator { if (method.deprecated) { pathMethod.deprecated = method.deprecated; } if (method.tags.length) { pathMethod.tags = method.tags; } if (method.security) { - pathMethod.security = method.security.map(s => ({ - [s.name || 'undefined']: s.scopes || [] - })); + + // console.log('CONFIG:', this.config.securityDefinitions); + // pathMethod.security = method.security.map(s => ({ + // [s.name || 'NOTFOUND']: s.scopes || [] + // })); + + // prepare an empty array for the pathMethod security fields + pathMethod.security = []; + + // process each security decorator in turn + method.security.forEach(securityDecoratorInfo => { + if (securityDecoratorInfo.name) { + const securityDefinition = this.config.securityDefinitions && this.config.securityDefinitions[securityDecoratorInfo.name]; + if (!securityDefinition) { + throw new Error(`No such securityDefinition '${securityDecoratorInfo.name}' named on method '${controllerName}.${method.method}'`); + } + // the scopes specified in the securityDecoratorInfo must align with those named in securityDefinitions + const missingScopes = _.difference(securityDecoratorInfo.scopes || [], Object.keys(securityDefinition.scopes || {})); + if (missingScopes.length > 0) { + throw new Error(`The securityDefinition '${securityDecoratorInfo.name}' named on method '${controllerName}.${method.method}' is missing specified scope(s): '${missingScopes.join(',')}'`); + } + pathMethod.security.push({[securityDecoratorInfo.name]: securityDecoratorInfo.scopes}); + + } else { + // when no name was specified, we need to find all those securityDefinitions whose scopes contain our specified scopes + + // prepare a list for all of the requiredScopes - even if there are none + // found - so that we know by the end if all the required scopes were discovered) + const requiredScopes = securityDecoratorInfo.scopes || []; + + // define remainingScopes, and reassign to subtract as we account for them + let remainingScopes = requiredScopes; + + // iterate over securityDefinitions, adding all with matching scopes + if (this.config.securityDefinitions) { + for (const securityDefinitionName in this.config.securityDefinitions) { + const securityDefinition = this.config.securityDefinitions[securityDefinitionName]; + const availableScopes = Object.keys(securityDefinition.scopes || {}); + + // find all scopes in the current security definition relevant to this decorator + const relevantScopes = _.intersection(requiredScopes, availableScopes); + + // remove relevantScopes from remainingScopes + remainingScopes = _.difference(remainingScopes, relevantScopes); + + if (relevantScopes.length || requiredScopes.length === 0) { + pathMethod.security.push({[securityDefinitionName]: relevantScopes}); + } + } + } else { + throw new Error('No securityDefinitions were defined in swagger.config.json, but one or more @Security decorators are present.'); + } + + if (remainingScopes.length > 0) { + throw new Error(`The security decorator on method '${controllerName}.${method.method}' could not find a match for the following scope(s): '${remainingScopes.join(',')}'`); + } else if (remainingScopes === requiredScopes) { + // if remainingScopes has not been reassigned, this means there were no securityDefinitions defined + // TODO: confirm this logic stands up - assumption might be wrong if _.difference returns the original input at any point + throw new Error('The securityDefinitions in swagger.config.json are empty, but one or more @Security decorators are present.'); + } + } + } + ); } this.handleMethodConsumes(method, pathMethod); From ffe7ad506e712eb1d9b9a98592f4b37b5ef70a3f Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Tue, 28 May 2019 15:43:25 +0100 Subject: [PATCH 03/15] improved wording of error messages in new @Security processing logic --- src/swagger/generator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/swagger/generator.ts b/src/swagger/generator.ts index 953cf28..0332f0a 100644 --- a/src/swagger/generator.ts +++ b/src/swagger/generator.ts @@ -136,12 +136,12 @@ export class SpecGenerator { if (securityDecoratorInfo.name) { const securityDefinition = this.config.securityDefinitions && this.config.securityDefinitions[securityDecoratorInfo.name]; if (!securityDefinition) { - throw new Error(`No such securityDefinition '${securityDecoratorInfo.name}' named on method '${controllerName}.${method.method}'`); + throw new Error(`Unknown securityDefinition '${securityDecoratorInfo.name}' used on method '${controllerName}.${method.method}'`); } // the scopes specified in the securityDecoratorInfo must align with those named in securityDefinitions const missingScopes = _.difference(securityDecoratorInfo.scopes || [], Object.keys(securityDefinition.scopes || {})); if (missingScopes.length > 0) { - throw new Error(`The securityDefinition '${securityDecoratorInfo.name}' named on method '${controllerName}.${method.method}' is missing specified scope(s): '${missingScopes.join(',')}'`); + throw new Error(`The securityDefinition '${securityDecoratorInfo.name}' used on method '${controllerName}.${method.method}' is missing specified scope(s): '${missingScopes.join(',')}'`); } pathMethod.security.push({[securityDecoratorInfo.name]: securityDecoratorInfo.scopes}); @@ -180,7 +180,7 @@ export class SpecGenerator { } else if (remainingScopes === requiredScopes) { // if remainingScopes has not been reassigned, this means there were no securityDefinitions defined // TODO: confirm this logic stands up - assumption might be wrong if _.difference returns the original input at any point - throw new Error('The securityDefinitions in swagger.config.json are empty, but one or more @Security decorators are present.'); + throw new Error('There are no securityDefinitions in swagger.config.json, but one or more @Security decorators have been used.'); } } } From 0c14064915ab14a89b9a996e6d7788dfdb1cd5fb Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Tue, 28 May 2019 16:53:01 +0100 Subject: [PATCH 04/15] removed completed TODO --- src/swagger/generator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/swagger/generator.ts b/src/swagger/generator.ts index 0332f0a..84e446f 100644 --- a/src/swagger/generator.ts +++ b/src/swagger/generator.ts @@ -179,7 +179,6 @@ export class SpecGenerator { throw new Error(`The security decorator on method '${controllerName}.${method.method}' could not find a match for the following scope(s): '${remainingScopes.join(',')}'`); } else if (remainingScopes === requiredScopes) { // if remainingScopes has not been reassigned, this means there were no securityDefinitions defined - // TODO: confirm this logic stands up - assumption might be wrong if _.difference returns the original input at any point throw new Error('There are no securityDefinitions in swagger.config.json, but one or more @Security decorators have been used.'); } } From 5b1175fc9ad35f8c40124d4a3561a34cfcee4804 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Tue, 28 May 2019 16:54:06 +0100 Subject: [PATCH 05/15] cleanup - remove reminder comment --- src/swagger/generator.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/swagger/generator.ts b/src/swagger/generator.ts index 84e446f..7aa37fe 100644 --- a/src/swagger/generator.ts +++ b/src/swagger/generator.ts @@ -122,12 +122,6 @@ export class SpecGenerator { if (method.deprecated) { pathMethod.deprecated = method.deprecated; } if (method.tags.length) { pathMethod.tags = method.tags; } if (method.security) { - - // console.log('CONFIG:', this.config.securityDefinitions); - // pathMethod.security = method.security.map(s => ({ - // [s.name || 'NOTFOUND']: s.scopes || [] - // })); - // prepare an empty array for the pathMethod security fields pathMethod.security = []; From 7671a8864a4e9fbc1d707c927cf9dacd239c4fc4 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Tue, 28 May 2019 17:03:45 +0100 Subject: [PATCH 06/15] removed whitespace changes --- .gitignore | 1 - src/metadata/controllerGenerator.ts | 12 ++------ src/metadata/methodGenerator.ts | 45 +++++++++-------------------- src/utils/decoratorUtils.ts | 10 ++----- 4 files changed, 19 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index cd2cf1b..84e01b0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ node_modules coverage *.log npm-debug.log* -.idea/ diff --git a/src/metadata/controllerGenerator.ts b/src/metadata/controllerGenerator.ts index 6fa4d8a..7bcb50b 100644 --- a/src/metadata/controllerGenerator.ts +++ b/src/metadata/controllerGenerator.ts @@ -79,17 +79,11 @@ export class ControllerGenerator { } private getMethodSecurity() { - if (!this.node.parent) { - throw new Error('Controller node doesn\'t have a valid parent source file.'); - } - if (!this.node.name) { - throw new Error('Controller node doesn\'t have a valid name.'); - } + if (!this.node.parent) { throw new Error('Controller node doesn\'t have a valid parent source file.'); } + if (!this.node.name) { throw new Error('Controller node doesn\'t have a valid name.'); } const securityDecorators = getDecorators(this.node, decorator => decorator.text === 'Security'); - if (!securityDecorators || !securityDecorators.length) { - return undefined; - } + if (!securityDecorators || !securityDecorators.length) { return undefined; } return securityDecorators.map(parseSecurityDecoratorArguments); } diff --git a/src/metadata/methodGenerator.ts b/src/metadata/methodGenerator.ts index 0379864..eaf07df 100644 --- a/src/metadata/methodGenerator.ts +++ b/src/metadata/methodGenerator.ts @@ -27,9 +27,7 @@ export class MethodGenerator { } public generate(): Method { - if (!this.isValid()) { - throw new Error('This isn\'t a valid controller method.'); - } + if (!this.isValid()) { throw new Error('This isn\'t a valid controller method.'); } const identifier = this.node.name as ts.Identifier; const type = resolveType(this.node.type, this.genericTypeMap); @@ -87,9 +85,7 @@ export class MethodGenerator { private processMethodDecorators() { const httpMethodDecorators = getDecorators(this.node, decorator => this.supportsPathMethod(decorator.text)); - if (!httpMethodDecorators || !httpMethodDecorators.length) { - return; - } + if (!httpMethodDecorators || !httpMethodDecorators.length) { return; } if (httpMethodDecorators.length > 1) { throw new Error(`Only one HTTP Method decorator in '${this.getCurrentLocation}' method is acceptable, Found: ${httpMethodDecorators.map(d => d.text).join(', ')}`); } @@ -112,9 +108,7 @@ export class MethodGenerator { private getMethodResponses(): ResponseType[] { const decorators = getDecorators(this.node, decorator => decorator.text === 'Response'); - if (!decorators || !decorators.length) { - return []; - } + if (!decorators || !decorators.length) { return []; } return decorators.map(decorator => { let description = ''; @@ -154,29 +148,20 @@ export class MethodGenerator { private getMethodSuccessResponseData(type: Type): ResponseData { switch (type.typeName) { - case 'void': - return {status: '204', type: type}; - case 'NewResource': - return {status: '201', type: type.typeArgument || type}; - case 'RequestAccepted': - return {status: '202', type: type.typeArgument || type}; - case 'MovedPermanently': - return {status: '301', type: type.typeArgument || type}; - case 'MovedTemporarily': - return {status: '302', type: type.typeArgument || type}; + case 'void': return { status: '204', type: type }; + case 'NewResource': return { status: '201', type: type.typeArgument || type }; + case 'RequestAccepted': return { status: '202', type: type.typeArgument || type }; + case 'MovedPermanently': return { status: '301', type: type.typeArgument || type }; + case 'MovedTemporarily': return { status: '302', type: type.typeArgument || type }; case 'DownloadResource': - case 'DownloadBinaryData': - return {status: '200', type: {typeName: 'buffer'}}; - default: - return {status: '200', type: type}; + case 'DownloadBinaryData': return { status: '200', type: { typeName: 'buffer' } }; + default: return { status: '200', type: type }; } } private getMethodSuccessExamples() { const exampleDecorators = getDecorators(this.node, decorator => decorator.text === 'Example'); - if (!exampleDecorators || !exampleDecorators.length) { - return undefined; - } + if (!exampleDecorators || !exampleDecorators.length) { return undefined; } if (exampleDecorators.length > 1) { throw new Error(`Only one Example decorator allowed in '${this.getCurrentLocation}' method.`); } @@ -222,9 +207,7 @@ export class MethodGenerator { private getDecoratorValues(decoratorName: string) { const tagsDecorators = getDecorators(this.node, decorator => decorator.text === decoratorName); - if (!tagsDecorators || !tagsDecorators.length) { - return []; - } + if (!tagsDecorators || !tagsDecorators.length) { return []; } if (tagsDecorators.length > 1) { throw new Error(`Only one ${decoratorName} decorator allowed in '${this.getCurrentLocation}' method.`); } @@ -235,9 +218,7 @@ export class MethodGenerator { private getMethodSecurity() { const securityDecorators = getDecorators(this.node, decorator => decorator.text === 'Security'); - if (!securityDecorators || !securityDecorators.length) { - return undefined; - } + if (!securityDecorators || !securityDecorators.length) { return undefined; } return securityDecorators.map(parseSecurityDecoratorArguments); } diff --git a/src/utils/decoratorUtils.ts b/src/utils/decoratorUtils.ts index 6ebc034..1c7322f 100644 --- a/src/utils/decoratorUtils.ts +++ b/src/utils/decoratorUtils.ts @@ -25,9 +25,7 @@ export function parseSecurityDecoratorArguments(decoratorData: DecoratorData): S export function getDecorators(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean): DecoratorData[] { const decorators = node.decorators; - if (!decorators || !decorators.length) { - return []; - } + if (!decorators || !decorators.length) { return []; } return decorators .map(d => { @@ -61,9 +59,7 @@ export function getDecorators(node: ts.Node, isMatching: (identifier: DecoratorD function getDecorator(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { const decorators = getDecorators(node, isMatching); - if (!decorators || !decorators.length) { - return; - } + if (!decorators || !decorators.length) { return; } return decorators[0]; } @@ -80,7 +76,7 @@ export function getDecoratorTextValue(node: ts.Node, isMatching: (identifier: De export function getDecoratorOptions(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { const decorator = getDecorator(node, isMatching); - return decorator && typeof decorator.arguments[1] === 'object' ? decorator.arguments[1] as { [key: string]: any } : undefined; + return decorator && typeof decorator.arguments[1] === 'object' ? decorator.arguments[1] as {[key: string]: any} : undefined; } export function isDecorator(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { From 655a48ec0e9658dc727bdf97d566d715e59e32b1 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Tue, 28 May 2019 17:07:44 +0100 Subject: [PATCH 07/15] clean up formatting changes to be in line with original project --- src/metadata/controllerGenerator.ts | 2 +- src/metadata/methodGenerator.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/metadata/controllerGenerator.ts b/src/metadata/controllerGenerator.ts index 7bcb50b..f7e8993 100644 --- a/src/metadata/controllerGenerator.ts +++ b/src/metadata/controllerGenerator.ts @@ -2,7 +2,7 @@ import * as ts from 'typescript'; import { Controller } from './metadataGenerator'; import { getSuperClass } from './resolveType'; import { MethodGenerator } from './methodGenerator'; -import {getDecorators, getDecoratorTextValue, parseSecurityDecoratorArguments} from '../utils/decoratorUtils'; +import { getDecorators, getDecoratorTextValue, parseSecurityDecoratorArguments } from '../utils/decoratorUtils'; import {normalizePath} from '../utils/pathUtils'; import * as _ from 'lodash'; diff --git a/src/metadata/methodGenerator.ts b/src/metadata/methodGenerator.ts index eaf07df..ebaad68 100644 --- a/src/metadata/methodGenerator.ts +++ b/src/metadata/methodGenerator.ts @@ -1,10 +1,10 @@ import * as ts from 'typescript'; -import {Method, ResponseData, ResponseType, Type} from './metadataGenerator'; -import {resolveType} from './resolveType'; -import {ParameterGenerator} from './parameterGenerator'; -import {getJSDocDescription, getJSDocTag, isExistJSDocTag} from '../utils/jsDocUtils'; -import {getDecorators, parseSecurityDecoratorArguments} from '../utils/decoratorUtils'; -import {normalizePath} from '../utils/pathUtils'; +import { Method, ResponseData, ResponseType, Type } from './metadataGenerator'; +import { resolveType } from './resolveType'; +import { ParameterGenerator } from './parameterGenerator'; +import { getJSDocDescription, getJSDocTag, isExistJSDocTag } from '../utils/jsDocUtils'; +import { getDecorators, parseSecurityDecoratorArguments } from '../utils/decoratorUtils'; +import { normalizePath } from '../utils/pathUtils'; import * as pathUtil from 'path'; export class MethodGenerator { From 3b6d1dfdc5002751ee498b5676041769b0ae0482 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Tue, 28 May 2019 17:11:47 +0100 Subject: [PATCH 08/15] the changes made in this branch constitute a feature, but do not make breaking changes; minor version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3026853..80d3d82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typescript-rest-swagger", - "version": "0.1.0", + "version": "0.2.0", "description": "Generate Swagger files from a typescript-rest project", "keywords": [ "typescript", From 180de2609ab364ff2f7de59b96a3fa7b2874fc38 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Wed, 29 May 2019 10:53:56 +0100 Subject: [PATCH 09/15] cleanup unneeded comments --- src/swagger/generator.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/swagger/generator.ts b/src/swagger/generator.ts index 7aa37fe..ced5976 100644 --- a/src/swagger/generator.ts +++ b/src/swagger/generator.ts @@ -141,12 +141,7 @@ export class SpecGenerator { } else { // when no name was specified, we need to find all those securityDefinitions whose scopes contain our specified scopes - - // prepare a list for all of the requiredScopes - even if there are none - // found - so that we know by the end if all the required scopes were discovered) const requiredScopes = securityDecoratorInfo.scopes || []; - - // define remainingScopes, and reassign to subtract as we account for them let remainingScopes = requiredScopes; // iterate over securityDefinitions, adding all with matching scopes From 20f0c2cfdd8cd749132a7688aa2a0c9a99c6c7dd Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Wed, 29 May 2019 16:10:30 +0100 Subject: [PATCH 10/15] added the same default empty array treatment to the logic flow for an explicitly named auth type that is used when the auth type has not been explicitly named --- src/swagger/generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/swagger/generator.ts b/src/swagger/generator.ts index ced5976..080cb3b 100644 --- a/src/swagger/generator.ts +++ b/src/swagger/generator.ts @@ -137,7 +137,7 @@ export class SpecGenerator { if (missingScopes.length > 0) { throw new Error(`The securityDefinition '${securityDecoratorInfo.name}' used on method '${controllerName}.${method.method}' is missing specified scope(s): '${missingScopes.join(',')}'`); } - pathMethod.security.push({[securityDecoratorInfo.name]: securityDecoratorInfo.scopes}); + pathMethod.security.push({[securityDecoratorInfo.name]: securityDecoratorInfo.scopes || []}); } else { // when no name was specified, we need to find all those securityDefinitions whose scopes contain our specified scopes From 3966a2226fb9414b56d84181f6cfc38c677d6eda Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Wed, 29 May 2019 16:17:44 +0100 Subject: [PATCH 11/15] corrected issues with correctly interpreting null or undefined values (by throwing an error in the case of names, and defaulting to empty arrays in the case of scopes) --- src/utils/decoratorUtils.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/utils/decoratorUtils.ts b/src/utils/decoratorUtils.ts index 1c7322f..4c3972f 100644 --- a/src/utils/decoratorUtils.ts +++ b/src/utils/decoratorUtils.ts @@ -1,14 +1,24 @@ import * as ts from 'typescript'; import {Security} from '../metadata/metadataGenerator'; +import {SyntaxKind} from 'typescript'; export function parseSecurityDecoratorArguments(decoratorData: DecoratorData): Security { if (decoratorData.arguments.length === 1) { // according to typescript-rest @Security decorator definition, when only one argument has been provided, // scopes must be the only parameter - return {name: undefined, scopes: parseScopesArgument( decoratorData.arguments[0] )}; - } else { + return {name: undefined, scopes: parseScopesArgument(decoratorData.arguments[0])}; + } else if (decoratorData.arguments.length === 2) { // in all other cases, maintain previous functionality - assume two parameters: name, scopes - return { name: decoratorData.arguments[0], scopes: parseScopesArgument( decoratorData.arguments[1] ) }; + + // nameArgument might be metadata which would result in a confusing error message + const nameArgument = decoratorData.arguments[0]; + if (typeof nameArgument !== 'string') { + throw new Error('name argument to @Security decorator must always be a string'); + } + + return {name: nameArgument, scopes: parseScopesArgument(decoratorData.arguments[1])}; + } else { + return {name: undefined, scopes: undefined}; } function parseScopesArgument(arg: any): Array | undefined { @@ -16,6 +26,8 @@ export function parseSecurityDecoratorArguments(decoratorData: DecoratorData): S if (typeof arg === 'string') { // wrap in an array for compatibility with upstream generator logic return [arg]; + } else if (arg && arg.kind === SyntaxKind.UndefinedKeyword || arg.kind === SyntaxKind.NullKeyword) { + return undefined; } else { // array from metadata needs to be extracted and converted to normal string array return arg ? (arg as any).elements.map((e: any) => e.text) : undefined; @@ -25,7 +37,9 @@ export function parseSecurityDecoratorArguments(decoratorData: DecoratorData): S export function getDecorators(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean): DecoratorData[] { const decorators = node.decorators; - if (!decorators || !decorators.length) { return []; } + if (!decorators || !decorators.length) { + return []; + } return decorators .map(d => { @@ -59,7 +73,9 @@ export function getDecorators(node: ts.Node, isMatching: (identifier: DecoratorD function getDecorator(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { const decorators = getDecorators(node, isMatching); - if (!decorators || !decorators.length) { return; } + if (!decorators || !decorators.length) { + return; + } return decorators[0]; } @@ -76,7 +92,7 @@ export function getDecoratorTextValue(node: ts.Node, isMatching: (identifier: De export function getDecoratorOptions(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { const decorator = getDecorator(node, isMatching); - return decorator && typeof decorator.arguments[1] === 'object' ? decorator.arguments[1] as {[key: string]: any} : undefined; + return decorator && typeof decorator.arguments[1] === 'object' ? decorator.arguments[1] as { [key: string]: any } : undefined; } export function isDecorator(node: ts.Node, isMatching: (identifier: DecoratorData) => boolean) { From 7404bc5780f5dc924af6597bca430868e96c17c0 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Wed, 29 May 2019 16:21:44 +0100 Subject: [PATCH 12/15] updated Security decorator to improve jsdocs and reflect the optional nature of the names parameter properly --- src/decorators.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/decorators.ts b/src/decorators.ts index af6e42c..3820576 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -67,11 +67,20 @@ export function Tags(...values: string[]): any { } /** - * Add a security constraint to method generated docs. - * @param {name} security name from securityDefinitions - * @param {scopes} security scopes from securityDefinitions + * Add a security constraint to method generated docs. All securityDefinitions containing the required scopes will be included. + * Scopes is optional, if omitted all defined securityDefinitions will be included, implying that any security type + * with any scope will suffice. + * @summary Add a security constraint to method generated docs. + * @param scopes security scopes from securityDefinitions */ -export function Security(name: string, scopes?: string[]): any { +export function Security( scopes?: Array | string): any; +/** + * Add a security constraint to method generated docs. Specific to the named securityDefinition. + * @summary Add a security constraint to method generated docs. + * @param name security name from securityDefinitions + * @param scopes security scopes from securityDefinitions + */ +export function Security( name: string, scopes?: Array | string ): any { return () => { return; }; } From d81d26d501cffe6b499d422e6472b2d98498f81d Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Wed, 29 May 2019 17:06:09 +0100 Subject: [PATCH 13/15] updated documentation to reflect changes to @Security usage --- README.MD | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/README.MD b/README.MD index 04b3e08..7b80652 100644 --- a/README.MD +++ b/README.MD @@ -165,17 +165,34 @@ class PeopleService { #### @Security -Add a security constraint to method generated docs, referencing the security name from securityDefinitions. +Add a security constraint to generated method docs, referencing the security name and any scopes (or, roles) from securityDefinitions. + `@Security` can be used at the controller and method level; if defined on both, method security overwrites controller security. Multiple security schemes may be specified to require all of them. +When used with a single parameter, this will be interpreted as the scopes, which can be a string or an array. With two parameters, the first parameter should be the name of a securityDefinition defined in swagger.config.json. The second parameter is the scopes, which can be a string or an array of strings. + +Note that where multiple scopes are specified, this implies that any one of those scopes will grant access. + ```typescript -@Path('people') -class PeopleService { - @Security('api_key') +@Path('secure') +class SecureService { + @Security('basic_auth', []) @GET - getPeople(@Param('name') name: string) { - // ... + getBasicAuthContent(@Param('id') id: string) { + // this method is only accessible by those authenticated with valid credentials for the basic_auth securityDefinition + } + + @Security('read_profile') + @GET + getProfile(@Param('id') id: string) { + // this method is only accessible by those authenticated with valid credentials with a grant for 'read_profile' for any securityDefinition containing the 'read_profile' scope + } + + @Security('oauth',['read_profile']) + @GET + getOauthSpecificProfile(@Param('id') id: string) { + // this method is only accessible by those authenticated with valid credentials for the oauth securityDefinition with a grant for the 'read_profile' scope } } ``` From 489be8f1f3264437639650c26cd22b14c9030f64 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Thu, 30 May 2019 11:07:09 +0100 Subject: [PATCH 14/15] -consolidated security decorator definition overloads --- src/decorators.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/decorators.ts b/src/decorators.ts index 3820576..1d81ce1 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -67,20 +67,16 @@ export function Tags(...values: string[]): any { } /** - * Add a security constraint to method generated docs. All securityDefinitions containing the required scopes will be included. + * Add a security constraint to method generated docs. + * Name is optional, if omitted all securityDefinitions containing the specified scopes will be included. Otherwise, specific to the named securityDefinition. * Scopes is optional, if omitted all defined securityDefinitions will be included, implying that any security type * with any scope will suffice. - * @summary Add a security constraint to method generated docs. - * @param scopes security scopes from securityDefinitions - */ -export function Security( scopes?: Array | string): any; -/** - * Add a security constraint to method generated docs. Specific to the named securityDefinition. + * NOTE: When supplying only one parameter, it will be interpreted as scopes. This is for typescript-rest compatibility. * @summary Add a security constraint to method generated docs. * @param name security name from securityDefinitions * @param scopes security scopes from securityDefinitions */ -export function Security( name: string, scopes?: Array | string ): any { +export function Security( name?: string, scopes?: Array | string ): any { return () => { return; }; } From 1ebe71d8b4cd8192d907e8632480f91e794340e7 Mon Sep 17 00:00:00 2001 From: Jack Hicks Date: Thu, 30 May 2019 11:07:50 +0100 Subject: [PATCH 15/15] altered tests to match new argument structure (and included securityDefinitions in the actual defaults for proper testing) --- test/data/apis.ts | 8 +++--- test/data/defaultOptions.ts | 52 +++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/test/data/apis.ts b/test/data/apis.ts index 1a10cf6..5ac9113 100644 --- a/test/data/apis.ts +++ b/test/data/apis.ts @@ -375,7 +375,7 @@ export class AbstractEntityEndpoint { } @Path('secure') -@swagger.Security('access_token') +@swagger.Security('access_token',[]) export class SecureEndpoint { @GET get(): string { @@ -383,15 +383,15 @@ export class SecureEndpoint { } @POST - @swagger.Security('user_email') + @swagger.Security('user_email',[]) post(): string { return 'Posted'; } } @Path('supersecure') -@swagger.Security('access_token') -@swagger.Security('user_email') +@swagger.Security('access_token',[]) +@swagger.Security('user_email',[]) export class SuperSecureEndpoint { @GET get(): string { diff --git a/test/data/defaultOptions.ts b/test/data/defaultOptions.ts index 47ab329..512853c 100644 --- a/test/data/defaultOptions.ts +++ b/test/data/defaultOptions.ts @@ -1,15 +1,41 @@ -import { SwaggerConfig } from './../../src/config'; +import {SwaggerConfig} from './../../src/config'; + export function getDefaultOptions(): SwaggerConfig { - return { - basePath: '/', - collectionFormat: 'multi', - description: 'Description of a test API', - entryFile: '', - host: 'localhost:3000', - license: 'MIT', - name: 'Test API', - outputDirectory: '', - version: '1.0.0', - yaml: false - }; + return { + basePath: '/', + collectionFormat: 'multi', + description: 'Description of a test API', + entryFile: '', + host: 'localhost:3000', + license: 'MIT', + name: 'Test API', + outputDirectory: '', + 'securityDefinitions': { + 'access_token': { + 'in': 'header', + 'name': 'authorization', + 'type': 'apiKey' + }, + 'api_key': { + 'in': 'query', + 'name': 'access_token', + 'type': 'apiKey', + }, + 'user_email': { + 'in': 'header', + 'name': 'x-user-email', + 'type': 'apiKey' + } + }, + 'spec': { + 'api_key': { + 'in': 'header', + 'name': 'api_key', + 'type': 'apiKey' + } + }, + version: '1.0.0', + yaml: false, + + }; }