From bb6b18c7a14abcbdee188d3f03eb1d11964293d4 Mon Sep 17 00:00:00 2001 From: Billie Hilton <587740+billiegoose@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:01:03 -0400 Subject: [PATCH 1/5] add tests --- .../security/__tests__/security.spec.ts | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/packages/http/src/validator/validators/security/__tests__/security.spec.ts b/packages/http/src/validator/validators/security/__tests__/security.spec.ts index f13e931a6..13c35a3d0 100644 --- a/packages/http/src/validator/validators/security/__tests__/security.spec.ts +++ b/packages/http/src/validator/validators/security/__tests__/security.spec.ts @@ -397,6 +397,63 @@ describe('validateSecurity', () => { }); }); + describe('when security scheme is optional', () => { + const securityScheme: HttpSecurityScheme[][] = [ + [ + { + id: faker.random.word(), + scheme: 'bearer', + type: 'http', + key: 'sec', + extensions: { 'x-test': faker.random.word() }, + }, + ], + [], // yeah that's how you do optional in OpenAPI + ]; + + it('passes if no security scheme is used', () => { + assertRight( + validateSecurity({ + element: { ...baseRequest, headers: {} }, + resource: { + security: securityScheme, + }, + }) + ); + }); + + it('passes the validation', () => { + assertRight( + validateSecurity({ + element: { ...baseRequest, headers: { authorization: 'Bearer abc123' } }, + resource: { + security: securityScheme, + }, + }) + ); + }); + + it('fails with an invalid security scheme error', () => { + assertLeft( + validateSecurity({ + element: { ...baseRequest, headers: { authorization: 'Basic abc123' } }, + resource: { + security: securityScheme, + }, + }), + res => + expect(res).toStrictEqual([ + { + code: 401, + message: 'Invalid security scheme used', + severity: DiagnosticSeverity.Error, + tags: ['Bearer', 'Basic realm="*"'], + }, + ]) + ); + }); + }); + describe('OR relation between security schemes', () => { const securityScheme: HttpSecurityScheme[][] = [ [ @@ -617,4 +674,168 @@ describe('validateSecurity', () => { }); }); }); + + describe('Mix of AND and OR security schemes', () => { + const headerScheme: HttpSecurityScheme = { + id: faker.random.word(), + in: 'header' as const, + type: 'apiKey' as const, + name: 'x-api-key' as const, + key: 'sec' as const, + extensions: { 'x-test': 'test' }, + }; + + const queryScheme: HttpSecurityScheme = { + id: faker.random.word(), + in: 'query' as const, + type: 'apiKey' as const, + name: 'x-api-key' as const, + key: 'sec' as const, + extensions: { 'x-test': 'test' }, + }; + + const cookieScheme: HttpSecurityScheme = { + id: faker.random.word(), + in: 'cookie' as const, + type: 'apiKey' as const, + name: 'x-api-key' as const, + key: 'sec' as const, + extensions: { 'x-test': 'test' }, + }; + + const bearerScheme: HttpSecurityScheme = { + id: faker.random.word(), + scheme: 'bearer', + type: 'http', + key: 'sec', + extensions: { 'x-test': faker.random.word() }, + }; + + const oauth2Scheme: HttpSecurityScheme = { + id: faker.random.word(), + type: 'oauth2', + flows: {}, + key: 'sec', + extensions: { 'x-test': faker.random.word() }, + }; + + const openIdScheme: HttpSecurityScheme = { + id: faker.random.word(), + type: 'openIdConnect', + openIdConnectUrl: 'https://google.it', + key: 'sec', + extensions: { 'x-test': faker.random.word() }, + }; + + const securityScheme: HttpSecurityScheme[][] = [ + // one of + [ + // all of + cookieScheme, + ], + [ + // all of + queryScheme, + oauth2Scheme, + ], + [ + // all of + bearerScheme, + headerScheme, + openIdScheme, + ], + ]; + + it('case 1 passes the validation', () => { + assertRight( + validateSecurity({ + element: { + ...baseRequest, + headers: { cookie: 'x-api-key=abc123' }, + }, + resource: { + security: securityScheme, + }, + }) + ); + }); + + it('case 2 passes the validation', () => { + assertRight( + validateSecurity({ + element: { + ...baseRequest, + headers: { authorization: 'Bearer abc123' }, + url: { path: '/', query: { 'x-api-key': 'abc123' } }, + }, + resource: { + security: securityScheme, + }, + }) + ); + }); + + it('case 3 passes the validation', () => { + assertRight( + validateSecurity({ + element: { + ...baseRequest, + headers: { 'x-api-key': 'abc123', authorization: 'Bearer abc123' }, + url: { path: '/', query: { 'x-api-key': 'abc123' } }, + }, + resource: { + security: securityScheme, + }, + }) + ); + }); + + it('fails with an invalid security scheme error 1', () => { + assertLeft( + validateSecurity({ + element: { + ...baseRequest, + headers: { 'x-api-key': 'abc123' }, + url: { path: '/', query: { 'x-api-key': 'abc123' } }, + }, + resource: { + security: securityScheme, + }, + }), + res => + expect(res).toStrictEqual([ + { + code: 401, + message: 'Invalid security scheme used', + severity: DiagnosticSeverity.Error, + tags: ['OAuth2', 'Bearer', 'OpenID'], + }, + ]) + ); + }); + + it('fails with an invalid security scheme error 2', () => { + assertLeft( + validateSecurity({ + element: { + ...baseRequest, + headers: { 'x-api-key': 'abc123' }, + url: { path: '/', query: { 'x-api-key': 'abc123' } }, + }, + resource: { + security: securityScheme, + }, + }), + res => + expect(res).toStrictEqual([ + { + code: 401, + message: 'Invalid security scheme used', + severity: DiagnosticSeverity.Error, + tags: ['OAuth2', 'Bearer', 'OpenID'], + }, + ]) + ); + }); + }); }); From b2c62bbb0cade657e93a5e6bed78fe7f3df382d2 Mon Sep 17 00:00:00 2001 From: Billie Hilton <587740+billiegoose@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:01:17 -0400 Subject: [PATCH 2/5] notes --- .../validator/validators/security/index.ts | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/http/src/validator/validators/security/index.ts b/packages/http/src/validator/validators/security/index.ts index f4c196a93..b0416545b 100644 --- a/packages/http/src/validator/validators/security/index.ts +++ b/packages/http/src/validator/validators/security/index.ts @@ -16,21 +16,37 @@ const EitherAltValidation = E.getAltValidation(getSemigroup()) const EitherApplicativeValidation = E.getApplicativeValidation(getSemigroup()); const eitherSequence = sequence(EitherApplicativeValidation); +export const validateSecurity: ValidatorFn, HeadersAndUrl> = ({ element, resource }) => + pipe( + O.fromNullable(resource.security), + O.chain(O.fromPredicate(isNonEmpty)), + O.fold( + () => E.right(element), + securitySchemes => + pipe( + getValidationResults(securitySchemes, element), + E.bimap( + e => [setErrorTag(e)], + () => element + ) + ) + ) + ); + function getValidationResults(securitySchemes: HttpSecurityScheme[][], input: HeadersAndUrl) { const [first, ...others] = getAuthenticationArray(securitySchemes, input); return others.reduce((prev, current) => EitherAltValidation.alt(prev, () => current), first); } -function setErrorTag(authResults: NonEmptyArray) { - const tags = authResults.map(authResult => authResult.tags || []); - return set(['tags'], flatten(tags), authResults[0]); -} - function getAuthenticationArray(securitySchemes: HttpSecurityScheme[][], input: HeadersAndUrl) { return securitySchemes.map(securitySchemePairs => { + // If securitySchemePairs.length === 0 then + // add an invalidator that only runs if the other results don't succeed + // that looks for "bad" auth headers. const authResults = securitySchemePairs.map(securityScheme => pipe( findSecurityHandler(securityScheme), + // TODO: figure out how to detect invalid or other security schemes here. E.chain(securityHandler => securityHandler(input, 'name' in securityScheme ? securityScheme.name : '')), E.mapLeft>(e => [e]) ) @@ -40,19 +56,7 @@ function getAuthenticationArray(securitySchemes: HttpSecurityScheme[][], input: }); } -export const validateSecurity: ValidatorFn, HeadersAndUrl> = ({ element, resource }) => - pipe( - O.fromNullable(resource.security), - O.chain(O.fromPredicate(isNonEmpty)), - O.fold( - () => E.right(element), - securitySchemes => - pipe( - getValidationResults(securitySchemes, element), - E.bimap( - e => [setErrorTag(e)], - () => element - ) - ) - ) - ); +function setErrorTag(authResults: NonEmptyArray) { + const tags = authResults.map(authResult => authResult.tags || []); + return set(['tags'], flatten(tags), authResults[0]); +} From e115a0cb709b87f3be31b6d375ab995ff07bb80d Mon Sep 17 00:00:00 2001 From: Billie Hilton <587740+billiegoose@users.noreply.github.com> Date: Fri, 29 Sep 2023 17:57:41 -0400 Subject: [PATCH 3/5] think I have optional working --- .../security/__tests__/security.spec.ts | 2 +- .../validators/security/handlers/index.ts | 5 +++++ .../validators/security/handlers/none.ts | 12 ++++++++++++ .../src/validator/validators/security/index.ts | 16 ++++++++++------ 4 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 packages/http/src/validator/validators/security/handlers/none.ts diff --git a/packages/http/src/validator/validators/security/__tests__/security.spec.ts b/packages/http/src/validator/validators/security/__tests__/security.spec.ts index 13c35a3d0..5465bbbc9 100644 --- a/packages/http/src/validator/validators/security/__tests__/security.spec.ts +++ b/packages/http/src/validator/validators/security/__tests__/security.spec.ts @@ -447,7 +447,7 @@ describe('validateSecurity', () => { code: 401, message: 'Invalid security scheme used', severity: DiagnosticSeverity.Error, - tags: ['Bearer', 'Basic realm="*"'], + tags: ['Bearer', 'None'], }, ]) ); diff --git a/packages/http/src/validator/validators/security/handlers/index.ts b/packages/http/src/validator/validators/security/handlers/index.ts index b6f628f54..7f777d41f 100644 --- a/packages/http/src/validator/validators/security/handlers/index.ts +++ b/packages/http/src/validator/validators/security/handlers/index.ts @@ -2,6 +2,7 @@ import { apiKeyInCookie, apiKeyInHeader, apiKeyInQuery } from './apiKey'; import { httpBasic } from './basicAuth'; import { httpDigest } from './digestAuth'; import { bearer, oauth2, openIdConnect } from './bearer'; +import { none } from './none'; import { HttpSecurityScheme, DiagnosticSeverity } from '@stoplight/types'; import { ValidateSecurityFn } from './utils'; import { Either, fromNullable } from 'fp-ts/Either'; @@ -20,6 +21,7 @@ const securitySchemeHandlers: { basic: ValidateSecurityFn; bearer: ValidateSecurityFn; }; + none: ValidateSecurityFn; } = { openIdConnect, oauth2, @@ -33,6 +35,7 @@ const securitySchemeHandlers: { basic: httpBasic, bearer, }, + none, }; function createDiagnosticFor(scheme: string): IPrismDiagnostic { @@ -55,3 +58,5 @@ export function findSecurityHandler(scheme: HttpSecurityScheme): Either) { + return get(inputHeaders, 'authorization') == undefined; +} + +export const none = (input: Pick) => when(isNoAuth(input.headers || {}), 'None'); diff --git a/packages/http/src/validator/validators/security/index.ts b/packages/http/src/validator/validators/security/index.ts index b0416545b..5dde9e7f1 100644 --- a/packages/http/src/validator/validators/security/index.ts +++ b/packages/http/src/validator/validators/security/index.ts @@ -4,7 +4,7 @@ import * as O from 'fp-ts/Option'; import { pipe } from 'fp-ts/function'; import { flatten } from 'lodash'; import { set } from 'lodash/fp'; -import { findSecurityHandler } from './handlers'; +import { findSecurityHandler, noneSecurityHandler } from './handlers'; import { NonEmptyArray, getSemigroup } from 'fp-ts/NonEmptyArray'; import { isNonEmpty, sequence } from 'fp-ts/Array'; import { IPrismDiagnostic, ValidatorFn } from '@stoplight/prism-core'; @@ -40,18 +40,22 @@ function getValidationResults(securitySchemes: HttpSecurityScheme[][], input: He function getAuthenticationArray(securitySchemes: HttpSecurityScheme[][], input: HeadersAndUrl) { return securitySchemes.map(securitySchemePairs => { - // If securitySchemePairs.length === 0 then - // add an invalidator that only runs if the other results don't succeed - // that looks for "bad" auth headers. const authResults = securitySchemePairs.map(securityScheme => pipe( findSecurityHandler(securityScheme), - // TODO: figure out how to detect invalid or other security schemes here. E.chain(securityHandler => securityHandler(input, 'name' in securityScheme ? securityScheme.name : '')), E.mapLeft>(e => [e]) ) ); - + // an empty array indicates "optional" security, + // in which case we run the special `None` validator + if (securitySchemePairs.length === 0) { + const optionalCheck = pipe( + noneSecurityHandler(input), + E.mapLeft>(e => [e]) + ); + authResults.push(optionalCheck); + } return eitherSequence(authResults); }); } From de64b1652d7173ab4d9d99b28651a8125455b731 Mon Sep 17 00:00:00 2001 From: Billie Hilton <587740+billiegoose@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:31:55 -0400 Subject: [PATCH 4/5] move stuff around to minimize diff --- .../validator/validators/security/index.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/http/src/validator/validators/security/index.ts b/packages/http/src/validator/validators/security/index.ts index 5dde9e7f1..b85827fd4 100644 --- a/packages/http/src/validator/validators/security/index.ts +++ b/packages/http/src/validator/validators/security/index.ts @@ -16,28 +16,16 @@ const EitherAltValidation = E.getAltValidation(getSemigroup()) const EitherApplicativeValidation = E.getApplicativeValidation(getSemigroup()); const eitherSequence = sequence(EitherApplicativeValidation); -export const validateSecurity: ValidatorFn, HeadersAndUrl> = ({ element, resource }) => - pipe( - O.fromNullable(resource.security), - O.chain(O.fromPredicate(isNonEmpty)), - O.fold( - () => E.right(element), - securitySchemes => - pipe( - getValidationResults(securitySchemes, element), - E.bimap( - e => [setErrorTag(e)], - () => element - ) - ) - ) - ); - function getValidationResults(securitySchemes: HttpSecurityScheme[][], input: HeadersAndUrl) { const [first, ...others] = getAuthenticationArray(securitySchemes, input); return others.reduce((prev, current) => EitherAltValidation.alt(prev, () => current), first); } +function setErrorTag(authResults: NonEmptyArray) { + const tags = authResults.map(authResult => authResult.tags || []); + return set(['tags'], flatten(tags), authResults[0]); +} + function getAuthenticationArray(securitySchemes: HttpSecurityScheme[][], input: HeadersAndUrl) { return securitySchemes.map(securitySchemePairs => { const authResults = securitySchemePairs.map(securityScheme => @@ -60,7 +48,19 @@ function getAuthenticationArray(securitySchemes: HttpSecurityScheme[][], input: }); } -function setErrorTag(authResults: NonEmptyArray) { - const tags = authResults.map(authResult => authResult.tags || []); - return set(['tags'], flatten(tags), authResults[0]); -} +export const validateSecurity: ValidatorFn, HeadersAndUrl> = ({ element, resource }) => + pipe( + O.fromNullable(resource.security), + O.chain(O.fromPredicate(isNonEmpty)), + O.fold( + () => E.right(element), + securitySchemes => + pipe( + getValidationResults(securitySchemes, element), + E.bimap( + e => [setErrorTag(e)], + () => element + ) + ) + ) + ); From 3307c69d5cb31b07499abf064d13037e7006584d Mon Sep 17 00:00:00 2001 From: Billie Hilton <587740+billiegoose@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:12:26 -0400 Subject: [PATCH 5/5] remove copied test --- .../security/__tests__/security.spec.ts | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/http/src/validator/validators/security/__tests__/security.spec.ts b/packages/http/src/validator/validators/security/__tests__/security.spec.ts index 5465bbbc9..56ec569c6 100644 --- a/packages/http/src/validator/validators/security/__tests__/security.spec.ts +++ b/packages/http/src/validator/validators/security/__tests__/security.spec.ts @@ -790,31 +790,7 @@ describe('validateSecurity', () => { ); }); - it('fails with an invalid security scheme error 1', () => { - assertLeft( - validateSecurity({ - element: { - ...baseRequest, - headers: { 'x-api-key': 'abc123' }, - url: { path: '/', query: { 'x-api-key': 'abc123' } }, - }, - resource: { - security: securityScheme, - }, - }), - res => - expect(res).toStrictEqual([ - { - code: 401, - message: 'Invalid security scheme used', - severity: DiagnosticSeverity.Error, - tags: ['OAuth2', 'Bearer', 'OpenID'], - }, - ]) - ); - }); - - it('fails with an invalid security scheme error 2', () => { + it('fails with an invalid security scheme error', () => { assertLeft( validateSecurity({ element: {