@sap/cds-mtxs
Version:
SAP Cloud Application Programming Model - Multitenancy library
293 lines (249 loc) • 9.74 kB
JavaScript
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
}
}