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
9 changes: 9 additions & 0 deletions src/decorators/security.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
/**
* Can be used to indicate that a method requires no security.
*/
export function NoSecurity(): Function {
return () => {
return;
};
}

/**
* @param {name} security name from securityDefinitions
*/
Expand Down
6 changes: 6 additions & 0 deletions src/metadataGeneration/controllerGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ export class ControllerGenerator {
}

private getSecurity(): Tsoa.Security[] {
const noSecurityDecorators = getDecorators(this.node, identifier => identifier.text === 'NoSecurity');
const securityDecorators = getDecorators(this.node, identifier => identifier.text === 'Security');

if (noSecurityDecorators?.length) {
throw new GenerateMetadataError(`NoSecurity decorator is unnecessary in '${this.node.name!.text}' class.`);
}

if (!securityDecorators || !securityDecorators.length) {
return [];
}
Expand Down
14 changes: 14 additions & 0 deletions src/metadataGeneration/methodGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,21 @@ export class MethodGenerator {
}

private getSecurity(): Tsoa.Security[] {
const noSecurityDecorators = this.getDecoratorsByIdentifier(this.node, 'NoSecurity');
const securityDecorators = this.getDecoratorsByIdentifier(this.node, 'Security');

if (noSecurityDecorators?.length > 1) {
throw new GenerateMetadataError(`Only one NoSecurity decorator allowed in '${this.getCurrentLocation}' method.`);
}

if (noSecurityDecorators?.length && securityDecorators?.length) {
throw new GenerateMetadataError(`NoSecurity decorator cannot be used in conjunction with Security decorator in '${this.getCurrentLocation}' method.`);
}

if (noSecurityDecorators?.length) {
return [];
}

if (!securityDecorators || !securityDecorators.length) {
return this.parentSecurity || [];
}
Expand Down
30 changes: 30 additions & 0 deletions tests/fixtures/controllers/noSecurityController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Get, Request, Response, Route, Security, NoSecurity } from '../../../src';
import { ErrorResponseModel, UserResponseModel } from '../../fixtures/testModel';

interface RequestWithUser {
user?: any;
}

@Security('tsoa_auth', ['write:pets', 'read:pets'])
@Route('NoSecurityTest')
export class NoSecurityTestController {
@Response<ErrorResponseModel>('default', 'Unexpected error')
@Security('api_key')
@Get()
public async GetWithApi(@Request() request: RequestWithUser): Promise<UserResponseModel> {
return Promise.resolve(request.user);
}

@Response<ErrorResponseModel>('404', 'Not Found')
@Get('Oauth')
public async GetWithImplicitSecurity(@Request() request: RequestWithUser): Promise<UserResponseModel> {
return Promise.resolve(request.user);
}

@Response<ErrorResponseModel>('404', 'Not Found')
@Get('Anonymous')
@NoSecurity()
public async GetWithNoSecurity(@Request() request: RequestWithUser): Promise<UserResponseModel> {
return Promise.resolve(request.user);
}
}
45 changes: 45 additions & 0 deletions tests/unit/swagger/pathGeneration/noSecurityRoutes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect } from 'chai';
import 'mocha';
import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
import { getDefaultExtendedOptions } from '../../../fixtures/defaultOptions';
import { VerifyPath } from '../../utilities/verifyPath';

describe('NoSecurity route generation', () => {
const metadata = new MetadataGenerator('./tests/fixtures/controllers/noSecurityController.ts').Generate();
const spec = new SpecGenerator2(metadata, getDefaultExtendedOptions()).GetSpec();

it('should generate a route with a named security', () => {
const path = verifyPath('/NoSecurityTest');

if (!path.get) {
throw new Error('No get operation.');
}

expect(path.get.security).to.deep.equal([{ api_key: [] }]);
});

it('should generate a route with scoped security', () => {
const path = verifyPath('/NoSecurityTest/Oauth');

if (!path.get) {
throw new Error('No get operation.');
}

expect(path.get.security).to.deep.equal([{ tsoa_auth: ['write:pets', 'read:pets'] }]);
});

it('should generate a route with no security', () => {
const path = verifyPath('/NoSecurityTest/Anonymous');

if (!path.get) {
throw new Error('No get operation.');
}

expect(path.get.security).to.deep.equal([]);
});

function verifyPath(route: string, isCollection?: boolean) {
return VerifyPath(spec, route, path => path.get, isCollection, false, '#/definitions/UserResponseModel');
}
});