Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate if server security has security schema and is valid #115

Merged
merged 5 commits into from
Jul 16, 2020
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
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