Skip to content

Commit

Permalink
feat: validate if server security has security schema and is valid (#115
Browse files Browse the repository at this point in the history
)
  • Loading branch information
derberg authored Jul 16, 2020
1 parent b70a34f commit ec8969a
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 8 deletions.
102 changes: 101 additions & 1 deletion lib/customValidators.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,108 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper
return true;
}

/**
* Validates if server security is declared properly and the name has a corresponding security schema definition in components with the same name
*
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was oryginally JSON or YAML
* @param {Array[String]} specialSecTypes list of security types that can have data in array
* @returns {Boolean} true in case the document is valid, otherwise throws ParserError
*/
function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, specialSecTypes) {
const srvs = parsedJSON.servers;
if (!srvs) return true;

const root = 'servers';
const srvsMap = new Map(Object.entries(srvs));

const missingSecSchema = new Map();
const invalidSecurityValues = new Map();

srvsMap.forEach((server, serverName) => {
const serverSecInfo = server.security;

if (!serverSecInfo) return true;

serverSecInfo.forEach(secObj => {
Object.keys(secObj).forEach(secName => {
const schema = findSecuritySchema(secName, parsedJSON.components);
const srvrSecurityPath = `${serverName}/security/${secName}`;

if (!schema.length) return missingSecSchema.set(srvrSecurityPath);

const schemaType = schema[1];
if (!isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName)) invalidSecurityValues.set(srvrSecurityPath, schemaType);
});
});
});

if (missingSecSchema.size) {
throw new ParserError({
type: validationError,
title: 'Server security name must correspond to a security scheme which is declared in the security schemes under the components object.',
parsedJSON,
validationErrors: groupValidationErrors(root, 'doesn\'t have a corresponding security schema under the components object', missingSecSchema, asyncapiYAMLorJSON, initialFormat)
});
}

if (invalidSecurityValues.size) {
throw new ParserError({
type: validationError,
title: 'Server security value must be an empty array if corresponding security schema type is not oauth2 or openIdConnect.',
parsedJSON,
validationErrors: groupValidationErrors(root, 'security info must have an empty array because its corresponding security schema type is', invalidSecurityValues, asyncapiYAMLorJSON, initialFormat)
});
}

return true;
}

/**
* Searches for server security corresponding object in security schema object
* @private
* @param {String} securityName name of the server security element that you want to localize in the security schema object
* @param {Object} components components object from the AsyncAPI document
* @returns {Array[String]} there are 2 elements in array, index 0 is the name of the security schema object and index 1 is it's type
*/
function findSecuritySchema(securityName, components) {
const secSchemes = components && components.securitySchemes;
const secSchemesMap = secSchemes ? new Map(Object.entries(secSchemes)) : new Map();
const schemaInfo = [];

//using for loop here as there is no point to iterate over all entries as it is enough to find first matching element
for (const [schemaName, schema] of secSchemesMap.entries()) {
if (schemaName === securityName) {
schemaInfo.push(schemaName, schema.type);
return schemaInfo;
}
}
return schemaInfo;
}

/**
* Validates if given server security is a proper empty array when security type requires it
* @private
* @param {String} schemaType security type, like httpApiKey or userPassword
* @param {Array[String]} specialSecTypes list of special types that do not have to be an empty array
* @param {Object} secObj server security object
* @param {String} secName name os server security object
* @returns {Array[String]} there are 2 elements in array, index 0 is the name of the security schema object and index 1 is it's type
*/
function isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName) {
if (!specialSecTypes.includes(schemaType)) {
const securityObjValue = secObj[String(secName)];

return !securityObjValue.length;
}

return true;
}

module.exports = {
validateChannelParams,
validateServerVariables,
validateOperationId
validateOperationId,
validateServerSecurity
};
8 changes: 6 additions & 2 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ const asyncapi = require('@asyncapi/specs');
const $RefParser = require('@apidevtools/json-schema-ref-parser');
const mergePatch = require('tiny-merge-patch').apply;
const ParserError = require('./errors/parser-error');
const { validateChannelParams, validateServerVariables, validateOperationId } = require('./customValidators.js');
const { validateChannelParams, validateServerVariables, validateOperationId, validateServerSecurity } = require('./customValidators.js');
const { toJS, findRefs, getLocationOf, improveAjvErrors } = require('./utils');
const AsyncAPIDocument = require('./models/asyncapi');

const DEFAULT_SCHEMA_FORMAT = 'application/vnd.aai.asyncapi;version=2.0.0';
const OPERATIONS = ['publish', 'subscribe'];
//the only security types that can have a non empty array in the server security item
const SPECIAL_SECURITY_TYPES = ['oauth2', 'openIdConnect'];
const PARSERS = {};

/**
Expand Down Expand Up @@ -149,7 +151,9 @@ function parseFromUrl(url, fetchOptions = {}, options) {
}

async function customDocumentOperations(js, asyncapiYAMLorJSON, initialFormat, options) {
if (js.servers) validateServerVariables(js, asyncapiYAMLorJSON, initialFormat);
validateServerVariables(js, asyncapiYAMLorJSON, initialFormat);
validateServerSecurity(js, asyncapiYAMLorJSON, initialFormat, SPECIAL_SECURITY_TYPES);

if (!js.channels) return;

validateChannelParams(js, asyncapiYAMLorJSON, initialFormat);
Expand Down
16 changes: 12 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,17 @@ const findNodeInAST = (ast, location) => {
let obj = ast;
for (const key of location) {
if (!Array.isArray(obj.children)) return;
const child = obj.children.find(c => c && c.type === 'Property' && c.key && c.key.value === utils.untilde(key));
let childArray;

const child = obj.children.find(c => {
if (!c) return;

if (c.type === 'Object') return childArray = c.children.find(a => a.key.value === utils.untilde(key));
return c.type === 'Property' && c.key && c.key.value === utils.untilde(key);
});

if (!child) return;
obj = child.value;
obj = childArray ? childArray.value : child.value;
}
return obj;
};
Expand Down Expand Up @@ -281,7 +289,7 @@ utils.getMissingProps = (arr, obj) => {
* @param {String} root name of the root element in the AsyncAPI document, for example channels
* @param {String} errorMessage the text of the custom error message that will follow the path that points the error
* @param {Map} errorElements map of error elements cause the validation error might happen in many places in the document.
* The key should have a path information where the error was found, the value holds information about error element
* The key should have a path information where the error was found, the value holds information about error element but it is not mandatory
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was oryginally JSON or YAML
* @returns {Array<Object>} Object has always 2 keys, title and location. Title is a combination of errorElement key + errorMessage + errorElement value.
Expand All @@ -294,7 +302,7 @@ utils.groupValidationErrors = (root, errorMessage, errorElements, asyncapiYAMLor
if (typeof val === 'string') val = utils.untilde(val);

errors.push({
title: `${ utils.untilde(key) } ${errorMessage}: ${val}`,
title: val ? `${ utils.untilde(key) } ${errorMessage}: ${val}` : `${ utils.untilde(key) } ${errorMessage}`,
location: utils.getLocationOf(`/${root}/${key}`, asyncapiYAMLorJSON, initialFormat)
});
});
Expand Down
Loading

0 comments on commit ec8969a

Please sign in to comment.