UNPKG

@sap/cds-mtxs

Version:

SAP Cloud Application Programming Model - Multitenancy library

278 lines (233 loc) 8.83 kB
const LinterMessage = require('./message') const Allowlist = require('./config') const LABELS = { service: 'Service', entity: 'Entity', aspect: 'Aspect', type: 'Type' } const NEW_FIELDS = 'new-fields' const FIELDS = 'fields' const NEW_ENTITIES = 'new-entities' module.exports = class AllowlistChecker { check(reflectedExtensionCsn, fullCsn, compileDir, mtxConfig) { if (!Object.keys(mtxConfig ?? {}).length) return [] const allowList = new Allowlist(mtxConfig, fullCsn) const messages = [] const foundEntityExt = {} // check entities for extensions if (reflectedExtensionCsn.extensions && allowList) { for (const extension of reflectedExtensionCsn.extensions) { this._checkEntity(extension, reflectedExtensionCsn, fullCsn, compileDir, allowList, messages, foundEntityExt) } } this._addEntityLimitWarnings(foundEntityExt, allowList, compileDir, messages) // check services const foundServiceExt = {} reflectedExtensionCsn.forall( element => { return ['entity', 'element', 'function', 'action'].includes(element.kind) }, (element, name, parent) => { if (allowList) { this._checkService( reflectedExtensionCsn, fullCsn, element, parent, { compileDir }, allowList, messages, foundServiceExt ) } } ) this._addServiceLimitWarnings(foundServiceExt, allowList, compileDir, messages) return messages } _checkEntity(extension, extCsn, fullCsn, compileDir, allowlist, messages, entityExt) { const extendedEntity = extension.extend if (extendedEntity) { if ( fullCsn && fullCsn.definitions && (!extCsn.definitions[extendedEntity] || extCsn.definitions[extendedEntity].kind !== 'entity') ) { const kind = this._getExtendedKind(fullCsn, extCsn, extendedEntity) if (!allowlist.isAllowed(kind, extendedEntity)) { // not allowed at all this._addColumnWarnings(extension, messages, compileDir, allowlist.getList(kind), kind) this._addElementWarnings(extension, messages, compileDir, allowlist.getList(kind), kind) } else { // might have limits entityExt[extendedEntity] = { elements: { ...extension.elements, ...entityExt[extendedEntity]?.elements }, columns: [...(extension.columns ?? []), ...(entityExt[extendedEntity]?.columns ?? [])], kind } this._addEntityFieldWarnings(extension, messages, compileDir, allowlist, kind) } } } } _getExtendedKind(fullCsn, reflectedExtensionCsn, extendedEntity) { const elementFromBase = fullCsn.definitions?.[extendedEntity] || reflectedExtensionCsn.definitions?.[extendedEntity] return elementFromBase ? elementFromBase.kind : null } _addElementWarnings(extension, messages, compileDir, allowlist, kind) { if (extension.elements) { for (const element in extension.elements) { messages.push( this._createAllowlistWarning( extension.extend, extension.elements[element], compileDir, allowlist, LABELS[kind] ) ) } } } _addColumnWarnings(extension, messages, compileDir, allowlist, kind) { // loop columns + elements if (extension.columns) { for (const column of extension.columns) { messages.push(this._createAllowlistWarning(extension.extend, column, compileDir, allowlist, LABELS[kind])) } } } // loop over all extensions per entity, add messages for all as soon as limit is exceeded _addEntityLimitWarnings(foundEntityExt, allowlist, compileDir, messages) { for (const entity in foundEntityExt) { const elements = foundEntityExt[entity].elements const columns = foundEntityExt[entity].columns const kind = foundEntityExt[entity].kind const limit = allowlist.getPermission(kind, entity)[NEW_FIELDS] if (limit == undefined) { // 0 is a valid limit continue } if ( (columns ? columns.length : 0) + (elements ? Object.keys(elements).length : 0) <= limit ) { continue } // loop columns + elements if (columns) { for (const column of columns) { messages.push(this._createElementLimitWarning(entity, column, compileDir, limit, LABELS[kind])) } } if (elements) { for (const element in elements) { messages.push( this._createLimitWarning(entity, element, elements[element], compileDir, limit, LABELS[kind]) ) } } } } _addEntityFieldWarnings(extension, messages, compileDir, allowlist, kind) { const allowedFields = allowlist.getPermission(kind, extension.extend)[FIELDS] if (!allowedFields || allowedFields.includes('*')) { return } const extendedFields = Object.keys(extension.elements ?? {}).filter(element => extension.elements[element].kind === 'extend') if (!extendedFields) { return } for (const fieldName of extendedFields) { if (!allowedFields.includes(fieldName)) { messages.push(this._createFieldExtensionWarning(fieldName, extension.extend, extension.elements[fieldName], compileDir, allowedFields)) } } } _addServiceLimitWarnings(foundServiceExt, allowlist, compileDir, messages) { if (!allowlist) { return } for (const service in foundServiceExt) { let extLimit = allowlist.getPermission('service', service)[NEW_ENTITIES] if (extLimit && extLimit < foundServiceExt[service].length) { // loop all extension for one service for (const element of foundServiceExt[service]) { messages.push(this._createElementLimitWarning(service, element, compileDir, extLimit, LABELS['service'])) } } } } _getParentName(element) { if (element.name) { const splitEntityName = element.name.split('.') if (splitEntityName.length > 1) { splitEntityName.pop() return splitEntityName.join('.') } } return null } _isDefinedInExtension(reflectedCsn, name) { return reflectedCsn.definitions ? !!reflectedCsn.definitions[name] : false } _isDefinedInBasemodel(fullCsn, name) { return !!this._getFromBasemodel(fullCsn, name) } _getFromBasemodel(fullCsn, name) { return fullCsn.definitions[name] } _checkService(reflectedExtensionCsn, fullCsn, element, parent, extension, allowlist, messages, foundExt) { if (parent && parent.kind && parent.kind !== 'service') { return } let parentName if (!parent) { parentName = this._getParentName(element) } else { parentName = this._getEntityName(parent) } // definition of element in extension itself if (!parentName) { return } // check if parent is defined in extension itself if (this._isDefinedInExtension(reflectedExtensionCsn, parentName)) { return } // REVISIT: if fullCsn includes extensions, _isDefinedInBasemodel always returns true // check if parent is defined in basemodel if (!this._isDefinedInBasemodel(fullCsn, parentName)) { return } if (allowlist.isAllowed('service', parentName)) { foundExt[parentName] = foundExt[parentName] || [] foundExt[parentName].push(element) return } messages.push( this._createAllowlistWarning(parentName, element, extension.compileDir, allowlist.service, LABELS['service']) ) } _createAllowlistWarning(entityName, element, compileDir, allowlist = {}, label) { let message = `${label} '${entityName}' must not be extended` message += `. Check ${label} allowlist: ${Object.keys(allowlist).length > 0 ? Object.keys(allowlist) : '<empty list>' }` return new LinterMessage(message, element) } _createFieldExtensionWarning(fieldName, entityName, element, compileDir, fieldlist) { let message = `Field '${fieldName}' of entity ${entityName} must not be extended` message += `. Check allowlist: ${fieldlist?.length ? fieldlist : '<empty list>'}` return new LinterMessage(message, element) } _createElementLimitWarning(entityName, element, compileDir, limit, label) { return this._createLimitWarning(entityName, element.name, element, compileDir, limit, label) } _createLimitWarning(entityName, elementName, element, compileDir, limit, label) { let message = `'${elementName}' exceeds extension limit of ${limit} for ${label} '${entityName}'` return new LinterMessage(message, element) } _getEntityName(entity) { return entity.extend ? entity.extend : entity.name } }