@sap/cds-mtxs
Version:
SAP Cloud Application Programming Model - Multitenancy library
169 lines (144 loc) • 6.57 kB
JavaScript
const LinterMessage = require('./message')
const { Allowlist, isBuiltIn } = require('./config')
// annotations with specific checks or messages
const checkedExtensionAnnotations = new Map([
['@requires', _createSecurityAnnotationWarning],
['@restrict', _createSecurityAnnotationWarning],
['@cds.persistence.journal', _createJournalAnnotationWarning],
['@mandatory', _createMandatoryAnnotationWarning],
['@readonly', _createMandatoryAnnotationWarning],
['@assert.notNull', _createMandatoryAnnotationWarning],
['@assert.range', _createMandatoryAnnotationWarning]
])
const criticalNewEntityAnnotations = /@sql.prepend|@sql.append|@cds.persistence.(?!skip)\w*/
const criticalExtensionAnnotations = new RegExp('^@(?:'
+ 'requires'
+ '|restrict'
+ '|readonly'
+ '|mandatory'
+ '|assert.*'
+ '|cds.persistence.*'
+ '|sql.append'
+ '|sql.prepend'
// service annotations
+ '|path'
+ '|impl'
+ '|cds.autoexpose'
+ '|cds.api.ignore'
+ '|odata.etag'
+ '|cds.query.limit'
+ '|cds.localized'
+ '|cds.valid.*'
+ '|cds.search)'
);
function locationString(element) {
const loc = element?.$location
if (!loc) return ''
const line = loc.line ? `${loc.line}:` : ''
return loc.col ? `${loc.file}:${line}${loc.col}:` : `${loc.file}:${line}`
}
function _createGenericAnnotationWarning(annotationName, { element, parent, name }) {
const message = `Annotation '${annotationName}' in '${
element.annotate || element.extend || element.name ||parent?.extend || parent.annotate || name
}' is not supported in extensions`
return new LinterMessage(locationString(element) + message, element)
}
function _createMandatoryAnnotationWarning(annotationName, { element, parent }) {
if (element.default || element[annotationName] === false) return
const message = `Annotation '${annotationName}' in '${
element.annotate || element.name || parent.extend || parent.annotate
}' is not supported in extensions without default value`
return new LinterMessage(locationString(element) + message, element)
}
function _createSecurityAnnotationWarning(annotationName, { element }) {
const message = `Security relevant annotation '${annotationName}' in '${
element.annotate || element.name
}' cannot be overwritten`
return new LinterMessage(locationString(element) + message, element)
}
function _createJournalAnnotationWarning(annotationName, { element }) {
const message = `Enabling schema evolution in extensions using '${annotationName}' in '${
element.annotate || element.name
}' not supported`
return new LinterMessage(locationString(element) + message, element)
}
function _createJournalEntityExtensionNotAllowedWarning(element) {
const message = `Extending entity '${element.extend}' is not supported as the corresponding database table has been enabled for schema evolution`
return new LinterMessage(locationString(element) + message, element)
}
module.exports = class AnnotationsChecker {
check(reflectedCsn, fullCsn, compileDir, mtxConfig = {}, originalExtCsn) {
const allowList = new Allowlist(mtxConfig, fullCsn)
if (!reflectedCsn.extensions && !reflectedCsn.definitions) {
return []
}
const annotationExtensions = []
const messages = []
// check annotations for extensions including fields
reflectedCsn.forall(
(element) => !isBuiltIn(element.name),
(element, name, parent) => {
if (Object.getOwnPropertyNames(element).filter(property =>
property.startsWith('@') && criticalExtensionAnnotations.test(property)).length) {
annotationExtensions.push({ element, name, parent })
}
if (element.extend) {
// check base entity for incompatible annotations
this._checkExtendedEntityAnnotations(fullCsn, element, messages) // checks e. g. for journal annotations in base entity
}
},
reflectedCsn.extensions ?? {}
)
// check entities and fields from new definitions
const annotatedDefinitions = []
reflectedCsn.forall(
(element) => !isBuiltIn(element.name),
(element, name, parent) => {
if (Object.getOwnPropertyNames(element).filter(property => criticalNewEntityAnnotations.test(property)).length) {
annotatedDefinitions.push({element, name, parent})
}
},
reflectedCsn.definitions ?? {}
)
for (const annotation of [...annotationExtensions, ...annotatedDefinitions]) {
const warnings = this._checkExtensionAnnotation(annotation, reflectedCsn, fullCsn, compileDir, allowList, originalExtCsn)
if (warnings?.length) {
messages.push(...warnings);
}
}
return messages
}
_checkExtensionAnnotation(annotation, extCsn, fullCsn, dir, allowList, originalExtCsn) {
const { element, parent, name } = annotation
const entityOrService = element.annotate ?? element.extend ?? parent?.annotate ?? parent?.extend ?? element.name ?? name
const annotationNames = Object.getOwnPropertyNames(element).filter(property => property.startsWith('@'))
if (!originalExtCsn.definitions[entityOrService] || annotationNames.filter(property => criticalNewEntityAnnotations.test(property)).length) {
const warnings = []
for (const annotationName of annotationNames) {
if (!criticalExtensionAnnotations.test(annotationName)) continue
// get element permissions from allowlist
const kind = this._getExtendedKind(fullCsn, extCsn, entityOrService)
const permissions = allowList.getPermission(kind, entityOrService)
// check if annotation is allowed
if (permissions && permissions.annotations && permissions.annotations.includes(annotationName)) return null
const fn = checkedExtensionAnnotations.get(annotationName) ?? _createGenericAnnotationWarning
const warning = fn(annotationName, annotation);
if (warning) {
warnings.push(warning);
}
}
return warnings
}
return null
}
_getExtendedKind(fullCsn, reflectedExtensionCsn, extendedEntity) {
const elementFromBase = fullCsn.definitions?.[extendedEntity] || reflectedExtensionCsn.definitions?.[extendedEntity]
return elementFromBase ? elementFromBase.kind : null
}
_checkExtendedEntityAnnotations(fullCsn, element, messages) {
const kind = fullCsn.definitions[element.extend]?.kind
if (kind === 'entity' && fullCsn.definitions[element.extend]?.['@cds.persistence.journal']) {
messages.push(_createJournalEntityExtensionNotAllowedWarning(element))
}
}
}