@sap/eslint-plugin-cds
Version:
ESLint plugin including recommended SAP Cloud Application Programming model and environment rules
165 lines (146 loc) • 5.44 kB
JavaScript
'use strict'
const { findFuzzy } = require('../utils/rules')
// See https://cap.cloud.sap/docs/guides/security/authorization#restrict-annotation
// The combination of these events is equivalent to the virtual 'WRITE' event.
const SAME_AS_WRITE_EVENT = [ 'CREATE', 'DELETE', 'UPDATE', 'UPSERT' ]
// Note that 'INSERT' is not meant to be used by users. They should use 'CREATE' instead.
const VALID_EVENTS = [ ...SAME_AS_WRITE_EVENT, 'READ', 'INSERT', '*', 'WRITE']
const TYPICAL_ISSUES = {
__proto__: null,
any: '*'
}
module.exports = {
meta: {
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
docs: {
description: '`@restrict.grant` must have valid values.',
category: 'Model Validation',
recommended: true,
url: 'https://cap.cloud.sap/docs/tools/cds-lint/rules/auth-valid-restrict-grant',
},
messages: {
invalidType: 'Invalid type for grant value. Must either be string or array of strings.',
invalidSingleType: 'Grant value must be a string.',
misspelledItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
unknownItem: `Invalid item '{{invalid}}'. Valid: [ {{candidates}} ].`,
replaceBy: `Replace grant values '{{values}}' with '{{name}}'.`,
missingEventOrAction: "Missing event/action on '{{name}}' for `@restrict.grant`.",
star: 'Grant value \'*\' overrides all other grants. Replace by \'*\' only.'
},
type: 'problem',
model: 'inferred'
},
create (context) {
return {
any: checkRestrictGrant
}
function checkRestrictGrant(e) {
if (!Array.isArray(e?.['@restrict']))
return
const node = context.getNode(e)
const file = e.$location.file
const actionNames = e.actions ? Object.keys(e.actions) : []
const validEventsAndActions = [ ...VALID_EVENTS, ...actionNames ]
for (const entry of e['@restrict']) {
if (entry?.grant !== undefined) {
if (!checkRestrictEntry(entry)) {
// max. one message per annotation, to avoid spamming the user
break
}
}
}
/**
* Check an entry of the `@restrict` array.
* Returns `true` if the value is valid, `false` otherwise.
*
* @param {object} entry
* @returns {boolean}
*/
function checkRestrictEntry( entry ) {
if (typeof entry.grant === 'string') {
return checkSingleGrantValue(entry.grant)
} else if (!Array.isArray(entry.grant)) {
// neither string nor array: report invalid type
context.report({ messageId: 'invalidType', node, file })
return false
} else {
if (entry.grant.length === 0) {
context.report({
messageId: 'missingEventOrAction',
data: { name: e.name }, node, file
})
return false
}
for (const value of entry.grant) {
if (!checkSingleGrantValue(value))
return false
}
// If grant values contain '*', '*' only is enough. It overrides everything.
if (entry.grant.length > 1 && entry.grant.includes('*')) {
context.report({ messageId: 'star', node, file })
return false
}
// If the given values include all of SAME_AS_WRITE_EVENT, the user can
// replace them with 'WRITE'.
const includesWrite = SAME_AS_WRITE_EVENT.every(value => entry.grant.includes(value))
if (includesWrite) {
context.report({
messageId: 'replaceBy',
data: { values: SAME_AS_WRITE_EVENT.join(', '), name: 'WRITE' },
node, file
})
return false
}
return true
}
}
/**
* Returns `true` if the value is valid, `false` otherwise.
*
* @param grant
* @returns {boolean}
*/
function checkSingleGrantValue(grant) {
if (typeof grant !== 'string') {
// single grant values must be strings
context.report({ messageId: 'invalidSingleType', node, file })
return false
}
if (grant.trim() === '') {
context.report({
messageId: 'missingEventOrAction',
data: { name: e.name }, node, file,
})
return false
}
if (!validEventsAndActions.includes(grant)) {
const candidates = TYPICAL_ISSUES[grant]
? [ TYPICAL_ISSUES[grant] ]
: findFuzzy(grant, validEventsAndActions.sort(), null, false, 2)
if (candidates.length === 0) {
context.report({
messageId: 'unknownItem',
data: { invalid: grant, candidates: validEventsAndActions.join(', ') }, node, file,
})
} else {
context.report({
messageId: 'misspelledItem',
data: { invalid: grant, candidates: candidates.join(', ') }, node, file,
})
}
return false
}
if (grant === 'INSERT') {
// special case: 'INSERT' is an internal event name. Users should use 'CREATE' instead.
context.report({
messageId: 'replaceBy',
data: { values: 'INSERT', name: 'CREATE' },
node, file
})
return false
}
return true
}
}
}
}