UNPKG

@sap/cds-mtxs

Version:

SAP Cloud Application Programming Model - Multitenancy library

293 lines (249 loc) 9.74 kB
const LinterMessage = require('./message') const { Allowlist, isBuiltIn } = require('./config') const LABELS = { service: 'Service', entity: 'Entity', aspect: 'Aspect', type: 'Type', namespace: 'Namespace' } 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) && !isBuiltIn(element.name) }, (element, name, parent) => { if (allowList) { this._checkServiceOrNamespace( reflectedExtensionCsn, fullCsn, element, parent, { compileDir }, allowList, messages, foundServiceExt ) } } ) this._addServiceOrNamespaceLimitWarnings(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: { ...Object.fromEntries( Object.entries(extension.elements ?? {}).filter(([, el]) => el.kind !== 'extend') ), ...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)) } } } _addServiceOrNamespaceLimitWarnings(foundExt, allowlist, compileDir, messages) { if (!allowlist) { return } for (const serviceOrNamespace in foundExt) { let extLimit = allowlist.getPermission('service', serviceOrNamespace)?.[NEW_ENTITIES] // falls back to all if ( (extLimit + 1) && extLimit < foundExt[serviceOrNamespace].length) { // loop all extension for one service for (const { element, label } of foundExt[serviceOrNamespace]) { messages.push(this._createElementLimitWarning(serviceOrNamespace, element, compileDir, extLimit, label)) } } } } _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?.[name] } _isDefinedInBasemodel(fullCsn, name) { return !!this._getFromBasemodel(fullCsn, name) } _getFromBasemodel(fullCsn, name) { return fullCsn.definitions[name] } _checkServiceOrNamespace(reflectedExtensionCsn, fullCsn, element, parent, extension, allowlist, messages, foundExt) { if (parent && parent.kind && parent.kind !== 'service') { return } let parentName if (!parent) { parentName = this._getParentName(element) // TODO: refine to extract namespace, check element.name } else { parentName = this._getEntityName(parent) } // definition of element in extension itself if (!parentName) { parentName = '' } // check if parent is defined in extension itself if (this._isDefinedInExtension(reflectedExtensionCsn, parentName)) { return } // REVISIT: if fullCsn includes extensions, _isDefinedInBasemodel always returns true -> only a problem for cross-referencences between extensions // check if parent is defined in basemodel let label if (!this._isDefinedInBasemodel(fullCsn, parentName)) { label = LABELS.namespace // REVISIT: might not be necessary here and can be completely skipped as before // namespace is only used as a placeholder to avoid fallback in allowlist but allow restrictions for namespaces if (allowlist.isAllowed('namespace', parentName)) { foundExt[parentName] = foundExt[parentName] || [] foundExt[parentName].push({ element, label }) return } } else { label = LABELS.service if (allowlist.isAllowed('service', parentName)) { foundExt[parentName] = foundExt[parentName] || [] foundExt[parentName].push({ element, label }) return } } messages.push( this._createAllowlistWarning(parentName, element, extension.compileDir, allowlist.service, label) ) } _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 ? label : ''} '${entityName}'` return new LinterMessage(message, element) } _getEntityName(entity) { return entity.extend ? entity.extend : entity.name } }