@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
281 lines (254 loc) • 8.86 kB
JavaScript
/**
* Copyright 2017 - 2023 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const stringValidator = require('validator');
const { LoggerFactory } = require('../utils');
let ruleId;
let logger;
module.exports = function (securityScheme, _opts, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}
return checkSecuritySchemeAttributes(
securityScheme,
context.path,
context.document
);
};
const API_KEY = 'apiKey';
const OAUTH2 = 'oauth2';
const HTTP = 'http';
const OPENID_CONNECT = 'openIdConnect';
const validTypes = [API_KEY, HTTP, OAUTH2, OPENID_CONNECT];
const validIns = ['query', 'header', 'cookie'];
/**
* This function will perform various checks to ensure that a securityScheme object is well-formed.
* Here is a summary:
* 1: 'type' is required and must be one of : apiKey, http, oauth2, openIdConnect`
* 2: 'name' is required for apiKey type
* 3: 'in' is required for apiKey type and must be one of: query, header or cookie
* 4: 'scheme' is required for http type
* 5: 'flows' object is required for oauth2 type
* 6: 'openIdConnectUrl' is required for openIdConnect type and must be a valid url
*
* Reference:
* https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#security-scheme-object
*
* @param {*} securityScheme the securityScheme object to be checked
* @param {*} path the array of path segments indicating the "location" of the securityScheme within the API definition
* @returns an array containing the violations found or [] if no violations
*/
function checkSecuritySchemeAttributes(securityScheme, path, doc) {
logger.debug(
`${ruleId}: checking securityScheme at location; ${path.join('.')}`
);
const errors = [];
const serviceUrl = getServiceUrl(doc);
const type = securityScheme.type;
if (!type) {
errors.push({
message: `Security scheme is missing required property: type`,
path,
});
} else if (!validTypes.includes(type)) {
errors.push({
message: `Security scheme property 'type' must be one of: ${validTypes.join(
', '
)}`,
path: [...path, 'type'],
});
} else if (type === HTTP) {
// http validation
if (!securityScheme.scheme) {
errors.push({
message: `Security scheme with type '${HTTP}' is missing required property: scheme`,
path,
});
}
} else if (type === API_KEY) {
// apiKey validation
const authIn = securityScheme.in;
if (!authIn) {
errors.push({
message: `Security scheme with type '${API_KEY}' is missing required property: in`,
path,
});
} else if (!validIns.includes(authIn)) {
errors.push({
message: `Security scheme property 'in' must be one of: ${validIns.join(
', '
)}`,
path: [...path, 'in'],
});
}
if (!securityScheme.name) {
errors.push({
message: `Security scheme with type '${API_KEY}' is missing required property: name`,
path,
});
}
} else if (type == OPENID_CONNECT) {
// openIdConnect validation
const openIdConnectUrl = securityScheme.openIdConnectUrl;
if (!openIdConnectUrl) {
errors.push({
message: `Security scheme with type '${OPENID_CONNECT}' is missing required property: openIdConnectUrl`,
path,
});
} else if (!isValidUrl(openIdConnectUrl, serviceUrl)) {
errors.push({
message: `Security scheme property 'openIdConnectUrl' must be a valid URL`,
path: [...path, 'openIdConnectUrl'],
});
}
} else if (type === OAUTH2) {
// oauth2 validation
const flows = securityScheme.flows;
if (!flows) {
errors.push({
message: `Security scheme with type '${OAUTH2}' is missing required property: flows`,
path,
});
} else {
// Validate the flows object.
// At least one flow type must be specified.
if (
!flows.implicit &&
!flows.authorizationCode &&
!flows.password &&
!flows.clientCredentials
) {
errors.push({
message: `oauth2 flow object must specify one or more of the following properties: implicit, password, clientCredentials or authorizationCode`,
path: [...path, 'flows'],
});
}
// Validate "authorizationCode" type flow.
let flow = flows.authorizationCode;
if (flow) {
if (!flow.tokenUrl) {
errors.push({
message: `oauth2 'authorizationCode' flow is missing required property: tokenUrl`,
path: [...path, 'flows', 'authorizationCode'],
});
} else if (!isValidUrl(flow.tokenUrl, serviceUrl)) {
errors.push({
message: `Security scheme property 'tokenUrl' must be a valid URL`,
path: [...path, 'flows', 'authorizationCode', 'tokenUrl'],
});
}
if (!flow.authorizationUrl) {
errors.push({
message: `oauth2 'authorizationCode' flow is missing required property: authorizationUrl`,
path: [...path, 'flows', 'authorizationCode'],
});
} else if (!isValidUrl(flow.authorizationUrl, serviceUrl)) {
errors.push({
message: `Security scheme property 'authorizationUrl' must be a valid URL`,
path: [...path, 'flows', 'authorizationCode', 'authorizationUrl'],
});
}
if (!flow.scopes) {
errors.push({
message: `oauth2 'authorizationCode' flow is missing required property: scopes`,
path: [...path, 'flows', 'authorizationCode'],
});
}
}
// Validate "password" type flow.
flow = flows.password;
if (flow) {
if (!flow.tokenUrl) {
errors.push({
message: `oauth2 'password' flow is missing required property: tokenUrl`,
path: [...path, 'flows', 'password'],
});
} else if (!isValidUrl(flow.tokenUrl, serviceUrl)) {
errors.push({
message: `Security scheme property 'tokenUrl' must be a valid URL`,
path: [...path, 'flows', 'password', 'tokenUrl'],
});
}
if (!flow.scopes) {
errors.push({
message: `oauth2 'password' flow is missing required property: scopes`,
path: [...path, 'flows', 'password'],
});
}
}
// Validate "clientCredentials" type flow.
flow = flows.clientCredentials;
if (flow) {
if (!flow.tokenUrl) {
errors.push({
message: `oauth2 'clientCredentials' flow is missing required property: tokenUrl`,
path: [...path, 'flows', 'clientCredentials'],
});
} else if (!isValidUrl(flow.tokenUrl, serviceUrl)) {
errors.push({
message: `Security scheme property 'tokenUrl' must be a valid URL`,
path: [...path, 'flows', 'clientCredentials', 'tokenUrl'],
});
}
if (!flow.scopes) {
errors.push({
message: `oauth2 'clientCredentials' flow is missing required property: scopes`,
path: [...path, 'flows', 'clientCredentials'],
});
}
}
// Validate "implicit" type flow.
flow = flows.implicit;
if (flow) {
if (!flow.authorizationUrl) {
errors.push({
message: `oauth2 'implicit' flow is missing required property: authorizationUrl`,
path: [...path, 'flows', 'implicit'],
});
} else if (!isValidUrl(flow.authorizationUrl, serviceUrl)) {
errors.push({
message: `Security scheme property 'authorizationUrl' must be a valid URL`,
path: [...path, 'flows', 'implicit', 'authorizationUrl'],
});
}
if (!flow.scopes) {
errors.push({
message: `oauth2 'implicit' flow is missing required property: scopes`,
path: [...path, 'flows', 'implicit'],
});
}
}
}
}
if (errors.length) {
logger.debug(
`${ruleId}: found these errors:\n${JSON.stringify(errors, null, 2)}`
);
} else {
logger.debug(`${ruleId}: PASSED!`);
}
return errors;
}
function getServiceUrl(doc) {
const openapi = doc.parserResult.data;
if (openapi.servers && openapi.servers.length > 0) {
return openapi.servers[0].url;
}
return '';
}
function isValidUrl(url, serviceUrl) {
if (typeof url !== 'string') {
return false;
}
// the openapi spec allows url properties to be relative to the url in "servers"
if (url.startsWith('/')) {
if (serviceUrl.endsWith('/')) {
url = serviceUrl + url.slice(1);
} else {
url = serviceUrl + url;
}
}
return stringValidator.isURL(url);
}