UNPKG

@sap/eslint-plugin-cds

Version:

ESLint plugin including recommended SAP Cloud Application Programming model and environment rules

165 lines (146 loc) 5.44 kB
'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 } } } }