UNPKG

@ibm-cloud/openapi-ruleset

Version:

Spectral ruleset for validating IBM Cloud services

228 lines (193 loc) 9.05 kB
/** * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ const { securitySchemes } = require('../../src/rules'); const { makeCopy, rootDocument, testRule, severityCodes, } = require('../test-utils'); const rule = securitySchemes; const ruleId = 'ibm-securityschemes'; const expectedSeverity = severityCodes.warning; const expectedMsgUndefinedScheme = 'An undefined security scheme is referenced'; const expectedMsgUndefinedScope = 'An undefined security scope is referenced'; const expectedMsgUnusedScheme = 'A security scheme is defined but never used'; const expectedMsgUnusedScope = 'A security scope is defined but never used'; const expectedMsgScopesNotSupported = 'For security scheme types that do not support scopes, the value must be an empty array'; describe(`Spectral rule: ${ruleId}`, () => { describe('Should not yield errors', () => { it('Clean spec', async () => { const results = await testRule(ruleId, rule, rootDocument); expect(results).toHaveLength(0); }); it('No security schemes or security reqmt objects', async () => { const testDocument = makeCopy(rootDocument); delete testDocument.components.securitySchemes; delete testDocument.security; delete testDocument.paths['/v1/drinks'].post.security; delete testDocument.paths['/v1/drinks'].get.security; delete testDocument.paths['/v1/drinks/{drink_id}'].get.security; delete testDocument.paths['/v1/movies'].post.security; delete testDocument.paths['/v1/movies'].get.security; delete testDocument.paths['/v1/movies/{movie_id}'].get.security; delete testDocument.paths['/v1/movies/{movie_id}'].put.security; delete testDocument.paths['/v1/cars'].post.security; delete testDocument.paths['/v1/cars/{car_id}'].get.security; delete testDocument.paths['/v1/cars/{car_id}'].patch.security; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(0); }); }); describe('Should yield errors', () => { it('No securitySchemes defined', async () => { const testDocument = makeCopy(rootDocument); // Remove all the security schemes. delete testDocument.components.securitySchemes; // All references to security schemes should now be flagged as undefined. const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(13); const expectedPaths = [ 'security.0.IAM', 'security.1.OpenIdScheme', 'paths./v1/drinks.post.security.0.DrinkScheme', 'paths./v1/drinks.get.security.0.Basic', 'paths./v1/drinks.get.security.0.DrinkScheme', 'paths./v1/drinks/{drink_id}.get.security.0.DrinkScheme', 'paths./v1/movies.post.security.0.MovieScheme', 'paths./v1/movies.get.security.0.MovieScheme', 'paths./v1/movies/{movie_id}.get.security.0.MovieScheme', 'paths./v1/movies/{movie_id}.put.security.0.MovieScheme', 'paths./v1/cars.post.security.0.IAM', 'paths./v1/cars/{car_id}.get.security.0.IAM', 'paths./v1/cars/{car_id}.patch.security.0.IAM', ]; for (let i = 0; i < results.length; i++) { expect(results[i].code).toBe(ruleId); expect(results[i].message).toBe(expectedMsgUndefinedScheme); expect(results[i].severity).toBe(expectedSeverity); expect(results[i].path.join('.')).toBe(expectedPaths[i]); } }); it('Reference to undefined security scheme - global', async () => { const testDocument = makeCopy(rootDocument); // Add "InvalidScheme" to the global security list. testDocument.security.push({ InvalidScheme: [], }); const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgUndefinedScheme); expect(results[0].severity).toBe(expectedSeverity); expect(results[0].path.join('.')).toBe('security.2.InvalidScheme'); }); it('Reference to undefined security scheme - operation', async () => { const testDocument = makeCopy(rootDocument); // Add "InvalidScheme" to an existing security list. testDocument.paths['/v1/drinks'].get.security.push({ InvalidScheme: [], }); const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgUndefinedScheme); expect(results[0].severity).toBe(expectedSeverity); expect(results[0].path.join('.')).toBe( 'paths./v1/drinks.get.security.1.InvalidScheme' ); }); it('Reference to undefined oath2 scope - global', async () => { const testDocument = makeCopy(rootDocument); // Reference undefined scope in global security requirement object. testDocument.security.push({ DrinkScheme: ['brewer'], }); const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgUndefinedScope); expect(results[0].severity).toBe(expectedSeverity); expect(results[0].path.join('.')).toBe('security.2.DrinkScheme.0'); }); it('Reference to undefined oath2 scope - operation', async () => { const testDocument = makeCopy(rootDocument); // Reference undefined scope in operation-level security requirement object. testDocument.paths['/v1/movies'].post.security[0].MovieScheme.push( 'producer' ); const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgUndefinedScope); expect(results[0].severity).toBe(expectedSeverity); expect(results[0].path.join('.')).toBe( 'paths./v1/movies.post.security.0.MovieScheme.1' ); }); it('Unused security scheme', async () => { const testDocument = makeCopy(rootDocument); // Add "UnusedScheme" to components.securitySchemes. testDocument.components.securitySchemes.UnusedScheme = { in: 'header', name: 'Authorization', type: 'http', scheme: 'Basic', }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgUnusedScheme); expect(results[0].severity).toBe(expectedSeverity); expect(results[0].path.join('.')).toBe( 'components.securitySchemes.UnusedScheme' ); }); it('Unused oauth2 scope', async () => { const testDocument = makeCopy(rootDocument); // Add "producer" scope to "MovieScheme" security scheme. testDocument.components.securitySchemes.MovieScheme.flows.authorizationCode.scopes[ 'producer' ] = 'Can produce Movies'; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgUnusedScope); expect(results[0].severity).toBe(expectedSeverity); expect(results[0].path.join('.')).toBe( 'components.securitySchemes.MovieScheme.flows.authorizationCode.scopes.producer' ); }); it('Unused oauth2 scope - name collision', async () => { const testDocument = makeCopy(rootDocument); // Add "drinker" scope to "MovieScheme" security scheme. // We'll now have two different schemes that define the "drinker" scope. // This should cause an error since the MovieScheme "drinker" scope will be unused. testDocument.components.securitySchemes.MovieScheme.flows.implicit.scopes[ 'drinker' ] = 'Can consume beverages'; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgUnusedScope); expect(results[0].severity).toBe(expectedSeverity); expect(results[0].path.join('.')).toBe( 'components.securitySchemes.MovieScheme.flows.implicit.scopes.drinker' ); }); it('Use scope with scheme type that doesnt support scopes', async () => { const testDocument = makeCopy(rootDocument); // Set a scope for a scheme type that doesn't support scopes. testDocument.security[0].IAM = ['notascope']; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); expect(results[0].code).toBe(ruleId); expect(results[0].message).toBe(expectedMsgScopesNotSupported); expect(results[0].severity).toBe(expectedSeverity); expect(results[0].path.join('.')).toBe('security.0.IAM'); }); }); });