@sap/eslint-plugin-cds 3.0.5 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +1 -1
- package/lib/api/index.js +4 -4
- package/lib/conf/all.js +17 -17
- package/lib/conf/experimental.js +12 -0
- package/lib/conf/index.js +12 -3
- package/lib/conf/recommended.js +13 -14
- package/lib/constants.js +2 -0
- package/lib/index.js +2 -1
- package/lib/parser.js +10 -1
- package/lib/rules/assoc2many-ambiguous-key.js +40 -11
- package/lib/rules/auth-no-empty-restrictions.js +38 -10
- package/lib/rules/auth-restrict-grant-service.js +29 -29
- package/lib/rules/auth-use-requires.js +27 -15
- package/lib/rules/auth-valid-restrict-grant.js +138 -82
- package/lib/rules/auth-valid-restrict-keys.js +34 -18
- package/lib/rules/auth-valid-restrict-to.js +57 -106
- package/lib/rules/auth-valid-restrict-where.js +44 -43
- package/lib/rules/extension-restrictions.js +11 -3
- package/lib/rules/index.js +5 -1
- package/lib/rules/latest-cds-version.js +5 -4
- package/lib/rules/no-db-keywords.js +14 -5
- package/lib/rules/no-dollar-prefixed-names.js +9 -2
- package/lib/rules/no-java-keywords.js +181 -0
- package/lib/rules/no-join-on-draft.js +9 -3
- package/lib/rules/sql-cast-suggestion.js +19 -15
- package/lib/rules/sql-null-comparison.js +60 -0
- package/lib/rules/start-elements-lowercase.js +6 -2
- package/lib/rules/start-entities-uppercase.js +12 -5
- package/lib/rules/valid-csv-header.js +33 -13
- package/lib/types.d.ts +4 -4
- package/lib/utils/Cache.js +4 -2
- package/lib/utils/Colors.js +2 -0
- package/lib/utils/LintError.js +17 -0
- package/lib/utils/createRule.js +160 -134
- package/lib/utils/csnTraversal.js +163 -0
- package/lib/utils/findFuzzy.js +21 -12
- package/lib/utils/getConfigPath.js +4 -2
- package/lib/utils/getConfiguredFileTypes.js +2 -0
- package/lib/utils/getFileExtensions.js +2 -0
- package/lib/utils/getProjectRootPath.js +53 -15
- package/lib/utils/isConfiguredFileType.js +8 -3
- package/lib/utils/rules.js +15 -9
- package/lib/utils/runRuleTester.js +69 -36
- package/package.json +1 -1
- package/lib/utils/genDocs.js +0 -346
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,55 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
+
## [3.1.1] - 2024-10-08
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `no-db-keywords` is no longer part of the 'recommended' rules,
|
|
14
|
+
as the cds-compiler takes care of quoting SQL keywords, if they are used as identifiers.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `auth-restrict-grant-service` can now handle invalid values for `@restrict`
|
|
19
|
+
- `auth-use-requires` now handles `null` values for `@restrict.grant.to`
|
|
20
|
+
- `auth-valid-restrict-to` is now more robust against invalid properties such as `__proto__`
|
|
21
|
+
and reduces the number of false positives
|
|
22
|
+
- `auth-valid-restrict-where` now handles and reports invalid value `@restrict: [{where: null}]`
|
|
23
|
+
- `auth-no-empty-restrictions` now handles invalid value `@restrict: [null]`
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## [3.1.0] - 2024-09-26
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- api:
|
|
31
|
+
+ rules now have a `name` property, containing the rule name
|
|
32
|
+
+ new exported property `parser`
|
|
33
|
+
- rules: there is now an `experimental` rule group, containing new rules that can be tested
|
|
34
|
+
- new experimental rules were added:
|
|
35
|
+
+ `@sap/cds/sql-null-comparison`
|
|
36
|
+
+ `@sap/cds/no-java-keywords`
|
|
37
|
+
- `auth-valid-restrict-grant` now proposes '*' when incorrect `@restrict.grant` value 'any' is used
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
|
|
41
|
+
- api: `genDocs` was removed
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- cli: Running `eslint` on the command line now runs `inferred` rules again
|
|
46
|
+
- `start-entities-uppercase` no longer reports false positives for elements
|
|
47
|
+
- Typescript errors in `lib/types.d.ts` were fixed
|
|
48
|
+
- Rule property `hasSuggestions: true` was removed from rules that did not have suggestions
|
|
49
|
+
- Custom rule tests using `runRuleTester` did not catch errors in files inside `valid/`.
|
|
50
|
+
Tests can now also be run with other test runners such as `mocha` and `node --test` instead of just `jest`
|
|
51
|
+
- `auth-` lint rules have been reworked to reduce the number of false positives and negatives
|
|
52
|
+
+ `auth-valid-restrict-where` no longer runs in quadratic time and now handles "expressions as annotation values"
|
|
53
|
+
+ `auth-no-empty-restrictions` now runs for actions and functions, too
|
|
54
|
+
+ `auth-use-requires` will not propose `@requires` anymore, if the `@restrict` has a `where` condition
|
|
55
|
+
+ `auth-valid-restrict-grants` incorrectly proposed to use `WRITE` instead of other events; it no longer crashes for invalid value types and now runs on all CSN artifacts
|
|
56
|
+
+ `auth-valid-restrict-keys` has improved reporting about misspelled vs unknown properties; it now runs on all CSN artifacts
|
|
57
|
+
|
|
9
58
|
## [3.0.5] - 2024-09-11
|
|
10
59
|
|
|
11
60
|
### Fixed
|
package/README.md
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
|
|
4
4
|
The [ESLint](https://eslint.org) plugin includes a set of recommended [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap) model and environment rules. The aim of CDS linting is to catch issues with CDS models and with the environment early. To use this module we recommend to install [@sap/cds-dk](https://www.npmjs.com/package/@sap/cds-dk) globally.
|
|
5
5
|
|
|
6
|
-
See the [CDS Linting documentation](https://cap.cloud.sap/docs/tools
|
|
6
|
+
See the [CDS Linting documentation](https://cap.cloud.sap/docs/tools/cds-lint/) for more details, or jump directly to a complete [list of rules](https://cap.cloud.sap/docs/tools/cds-lint/#cds-lint-rules). CAP provides a set of recommended rules. On top, you can create your own, application-specific rules.
|
package/lib/api/index.js
CHANGED
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
* Our custom ESLint plugin API should:
|
|
5
5
|
* - Expose 'createRule' and 'runRuleTester' to
|
|
6
6
|
* support the addition of *custom* CDS Lint rules at runtime
|
|
7
|
-
* - Expose 'getFileExtensions'
|
|
7
|
+
* - Expose 'getFileExtensions' for usage in
|
|
8
8
|
* 'cds lint' client (@sap/cds-dk)
|
|
9
9
|
* - Expose 'parserPath' for CDS Lint rule unit tests with ESLint's ruleTester
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const runRuleTester = require('../utils/runRuleTester')
|
|
13
13
|
const createRule = require('../utils/createRule')
|
|
14
|
-
const genDocs = require('../utils/genDocs')
|
|
15
14
|
const getConfigPath = require('../utils/getConfigPath')
|
|
16
15
|
const getConfiguredFileTypes = require('../utils/getConfiguredFileTypes')
|
|
17
16
|
const parserPath = require.resolve('../parser')
|
|
17
|
+
const parser = require('../parser')
|
|
18
18
|
|
|
19
19
|
module.exports = {
|
|
20
20
|
runRuleTester,
|
|
21
21
|
createRule,
|
|
22
|
-
genDocs,
|
|
23
22
|
getConfigPath,
|
|
24
23
|
getFileExtensions: getConfiguredFileTypes,
|
|
25
|
-
parserPath
|
|
24
|
+
parserPath,
|
|
25
|
+
parser,
|
|
26
26
|
}
|
package/lib/conf/all.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
-
'@sap/cds/assoc2many-ambiguous-key':
|
|
5
|
-
'@sap/cds/auth-no-empty-restrictions':
|
|
6
|
-
'@sap/cds/auth-use-requires':
|
|
7
|
-
'@sap/cds/auth-restrict-grant-service':
|
|
8
|
-
'@sap/cds/auth-valid-restrict-grant':
|
|
9
|
-
'@sap/cds/auth-valid-restrict-keys':
|
|
10
|
-
'@sap/cds/auth-valid-restrict-to':
|
|
11
|
-
'@sap/cds/auth-valid-restrict-where':
|
|
12
|
-
'@sap/cds/latest-cds-version':
|
|
13
|
-
'@sap/cds/no-db-keywords':
|
|
14
|
-
'@sap/cds/no-dollar-prefixed-names':
|
|
15
|
-
'@sap/cds/no-join-on-draft':
|
|
16
|
-
'@sap/cds/sql-cast-suggestion':
|
|
17
|
-
'@sap/cds/start-elements-lowercase':
|
|
18
|
-
'@sap/cds/start-entities-uppercase':
|
|
19
|
-
'@sap/cds/valid-csv-header':
|
|
20
|
-
'@sap/cds/extension-restrictions':
|
|
4
|
+
'@sap/cds/assoc2many-ambiguous-key': 'error',
|
|
5
|
+
'@sap/cds/auth-no-empty-restrictions': 'error',
|
|
6
|
+
'@sap/cds/auth-use-requires': 'error',
|
|
7
|
+
'@sap/cds/auth-restrict-grant-service': 'error',
|
|
8
|
+
'@sap/cds/auth-valid-restrict-grant': 'error',
|
|
9
|
+
'@sap/cds/auth-valid-restrict-keys': 'error',
|
|
10
|
+
'@sap/cds/auth-valid-restrict-to': 'error',
|
|
11
|
+
'@sap/cds/auth-valid-restrict-where': 'error',
|
|
12
|
+
'@sap/cds/latest-cds-version': 'error',
|
|
13
|
+
'@sap/cds/no-db-keywords': 'error',
|
|
14
|
+
'@sap/cds/no-dollar-prefixed-names': 'error',
|
|
15
|
+
'@sap/cds/no-join-on-draft': 'error',
|
|
16
|
+
'@sap/cds/sql-cast-suggestion': 'error',
|
|
17
|
+
'@sap/cds/start-elements-lowercase': 'error',
|
|
18
|
+
'@sap/cds/start-entities-uppercase': 'error',
|
|
19
|
+
'@sap/cds/valid-csv-header': 'error',
|
|
20
|
+
'@sap/cds/extension-restrictions': 'error',
|
|
21
21
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Experimental Rules
|
|
4
|
+
// ------------------
|
|
5
|
+
// New rules that we want to publish, but don't activate per default.
|
|
6
|
+
// We want to give users the chance to test them, before moving them
|
|
7
|
+
// to "all" or even "recommended".
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
'@sap/cds/sql-null-comparison': 'warn',
|
|
11
|
+
'@sap/cds/no-java-keywords': 'error',
|
|
12
|
+
}
|
package/lib/conf/index.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
2
4
|
const { FILES, GLOBALS, PLUGIN_NAME } = require('../constants')
|
|
3
5
|
const { parserPath } = require('../api')
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} plugin Plugin implementation used for new configuration layout.
|
|
9
|
+
* @param {string} configName
|
|
10
|
+
* @param {boolean} [isLegacy]
|
|
11
|
+
*/
|
|
12
|
+
function _createConfig (plugin, configName, isLegacy = false) {
|
|
6
13
|
const config = require(path.join(__dirname, configName))
|
|
7
|
-
if (
|
|
14
|
+
if (isLegacy) {
|
|
8
15
|
return {
|
|
9
16
|
root: true,
|
|
10
17
|
globals: GLOBALS,
|
|
@@ -19,6 +26,7 @@ function _createConfig (plugin, configName, legacy = false) {
|
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
return {
|
|
29
|
+
name: `@sap/cds/${configName}`,
|
|
22
30
|
languageOptions: {
|
|
23
31
|
globals: GLOBALS,
|
|
24
32
|
parser: require(parserPath)
|
|
@@ -37,6 +45,7 @@ module.exports = function (plugin) {
|
|
|
37
45
|
return {
|
|
38
46
|
all: _createConfig(plugin, 'all'),
|
|
39
47
|
recommended: _createConfig(plugin, 'recommended'),
|
|
48
|
+
experimental: _createConfig(plugin, 'experimental'),
|
|
40
49
|
// Legacy configs (for backwards compatibility)
|
|
41
50
|
'all-legacy': _createConfig(plugin, 'all', true),
|
|
42
51
|
'recommended-legacy': _createConfig(plugin, 'recommended', true)
|
package/lib/conf/recommended.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
-
'@sap/cds/assoc2many-ambiguous-key':
|
|
5
|
-
'@sap/cds/auth-no-empty-restrictions':
|
|
6
|
-
'@sap/cds/auth-use-requires':
|
|
7
|
-
'@sap/cds/auth-restrict-grant-service':
|
|
8
|
-
'@sap/cds/auth-valid-restrict-grant':
|
|
9
|
-
'@sap/cds/auth-valid-restrict-keys':
|
|
10
|
-
'@sap/cds/auth-valid-restrict-to':
|
|
11
|
-
'@sap/cds/auth-valid-restrict-where':
|
|
12
|
-
'@sap/cds/no-
|
|
13
|
-
'@sap/cds/no-
|
|
14
|
-
'@sap/cds/
|
|
15
|
-
'@sap/cds/
|
|
16
|
-
'@sap/cds/
|
|
17
|
-
'@sap/cds/extension-restrictions': 2
|
|
4
|
+
'@sap/cds/assoc2many-ambiguous-key': 'error',
|
|
5
|
+
'@sap/cds/auth-no-empty-restrictions': 'error',
|
|
6
|
+
'@sap/cds/auth-use-requires': 'warn',
|
|
7
|
+
'@sap/cds/auth-restrict-grant-service': 'error',
|
|
8
|
+
'@sap/cds/auth-valid-restrict-grant': 'warn',
|
|
9
|
+
'@sap/cds/auth-valid-restrict-keys': 'warn',
|
|
10
|
+
'@sap/cds/auth-valid-restrict-to': 'warn',
|
|
11
|
+
'@sap/cds/auth-valid-restrict-where': 'warn',
|
|
12
|
+
'@sap/cds/no-dollar-prefixed-names': 'warn',
|
|
13
|
+
'@sap/cds/no-join-on-draft': 'warn',
|
|
14
|
+
'@sap/cds/sql-cast-suggestion': 'warn',
|
|
15
|
+
'@sap/cds/valid-csv-header': 'warn',
|
|
16
|
+
'@sap/cds/extension-restrictions': 'error',
|
|
18
17
|
}
|
package/lib/constants.js
CHANGED
package/lib/index.js
CHANGED
package/lib/parser.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Custom ESLint parser:
|
|
3
5
|
* https://eslint.org/docs/developer-guide/working-with-custom-parsers
|
|
@@ -28,7 +30,7 @@ module.exports = {
|
|
|
28
30
|
const messages = []
|
|
29
31
|
try {
|
|
30
32
|
compiledModel = cds.parse(code)
|
|
31
|
-
} catch
|
|
33
|
+
} catch {
|
|
32
34
|
// Do nothing
|
|
33
35
|
}
|
|
34
36
|
if (compiledModel) {
|
|
@@ -124,6 +126,7 @@ module.exports = {
|
|
|
124
126
|
let loc
|
|
125
127
|
if (obj) {
|
|
126
128
|
let name = obj.name
|
|
129
|
+
// TODO: 'action'/'function' not correct for bound action/function
|
|
127
130
|
if (['action', 'entity', 'function', 'service'].includes(obj.kind)) {
|
|
128
131
|
name = splitDefName(obj).name
|
|
129
132
|
}
|
|
@@ -143,7 +146,9 @@ module.exports = {
|
|
|
143
146
|
|
|
144
147
|
/**
|
|
145
148
|
* Generates dummy AST with just single Program node
|
|
149
|
+
*
|
|
146
150
|
* @param code Parse file contents
|
|
151
|
+
* @param {object} [loc]
|
|
147
152
|
* @returns AST
|
|
148
153
|
*/
|
|
149
154
|
function createProgramAST (code, loc) {
|
|
@@ -168,6 +173,10 @@ function createProgramAST (code, loc) {
|
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
175
|
|
|
176
|
+
/**
|
|
177
|
+
* @param {object} dictFiles
|
|
178
|
+
* @param options
|
|
179
|
+
*/
|
|
171
180
|
function compileModelFromDict (dictFiles, options) {
|
|
172
181
|
let reflectedModel
|
|
173
182
|
const messages = []
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const cds = require('@sap/cds')
|
|
2
4
|
|
|
3
5
|
/** @type {import('../types').Rule} */
|
|
@@ -9,7 +11,11 @@ module.exports = {
|
|
|
9
11
|
description:
|
|
10
12
|
'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
|
|
11
13
|
category: 'Model Validation',
|
|
12
|
-
recommended: true
|
|
14
|
+
recommended: true,
|
|
15
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/assoc2many-ambiguous-key',
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
ambiguous: `Ambiguous key in '{{name}}'. Element '{{column-name}}' leads to multiple entries so that key '{{key-name}}' is not unique.`,
|
|
13
19
|
},
|
|
14
20
|
type: 'problem',
|
|
15
21
|
model: 'inferred'
|
|
@@ -22,14 +28,23 @@ module.exports = {
|
|
|
22
28
|
const m = context.getModel()
|
|
23
29
|
if (!m) return
|
|
24
30
|
if (m && m.definitions) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
try {
|
|
32
|
+
csnOdata = cds.compile.for.odata(m)
|
|
33
|
+
const csnOdataLinked = cds.linked(csnOdata)
|
|
34
|
+
associationCardinalityFlaw(csnOdataLinked, context)
|
|
35
|
+
} catch {
|
|
36
|
+
// FIXME: there are currently too many issues with this rule, e.g.
|
|
37
|
+
// assumptions that properties exist, etc.
|
|
38
|
+
}
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @param {object} csn
|
|
46
|
+
* @param {CDSRuleContext} context
|
|
47
|
+
*/
|
|
33
48
|
function associationCardinalityFlaw (csn, context) {
|
|
34
49
|
processEntity(csn, (definition, sourceEntity, sourceAlias) => {
|
|
35
50
|
let refCardinalityMult = false
|
|
@@ -52,7 +67,7 @@ function associationCardinalityFlaw (csn, context) {
|
|
|
52
67
|
refPlainElement = true
|
|
53
68
|
}
|
|
54
69
|
},
|
|
55
|
-
|
|
70
|
+
column => {
|
|
56
71
|
if (
|
|
57
72
|
definition.keys &&
|
|
58
73
|
Object.keys(definition.keys).length === 1 &&
|
|
@@ -65,7 +80,8 @@ function associationCardinalityFlaw (csn, context) {
|
|
|
65
80
|
const keyLoc = context.getLocation(keyName, key, csn)
|
|
66
81
|
const colName = column.as ? column.as : column.name
|
|
67
82
|
context.report({
|
|
68
|
-
|
|
83
|
+
messageId: 'ambiguous',
|
|
84
|
+
data: { name: definition.name, 'column-name': colName, 'key-name': keyName },
|
|
69
85
|
loc: keyLoc,
|
|
70
86
|
file: key.$location.file
|
|
71
87
|
})
|
|
@@ -75,8 +91,12 @@ function associationCardinalityFlaw (csn, context) {
|
|
|
75
91
|
})
|
|
76
92
|
}
|
|
77
93
|
|
|
94
|
+
/**
|
|
95
|
+
* @param {object} csn
|
|
96
|
+
* @param {Function} eachCallback
|
|
97
|
+
*/
|
|
78
98
|
function processEntity (csn, eachCallback) {
|
|
79
|
-
Object.keys(csn.definitions).forEach(
|
|
99
|
+
Object.keys(csn.definitions).forEach(name => {
|
|
80
100
|
if (name.startsWith('localized.')) {
|
|
81
101
|
return
|
|
82
102
|
}
|
|
@@ -99,7 +119,7 @@ function processEntity (csn, eachCallback) {
|
|
|
99
119
|
} else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
|
|
100
120
|
// Join
|
|
101
121
|
sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join('_')]
|
|
102
|
-
definition.query.SELECT.from.args.forEach(
|
|
122
|
+
definition.query.SELECT.from.args.forEach(arg => {
|
|
103
123
|
sourceAlias.push({
|
|
104
124
|
from: arg.ref.join('_'),
|
|
105
125
|
as: arg.as || arg.ref.slice(-1)[0].split('.').pop()
|
|
@@ -114,16 +134,25 @@ function processEntity (csn, eachCallback) {
|
|
|
114
134
|
})
|
|
115
135
|
}
|
|
116
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @param {object} csn
|
|
139
|
+
* @param {object} definition
|
|
140
|
+
* @param {object} sourceEntity
|
|
141
|
+
* @param {string} sourceAlias
|
|
142
|
+
* @param {Function} beforeCallback
|
|
143
|
+
* @param {Function} eachCallback
|
|
144
|
+
* @param {Function} afterCallback
|
|
145
|
+
*/
|
|
117
146
|
function processElement (csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
|
|
118
|
-
definition.query.SELECT.columns.forEach(
|
|
147
|
+
definition.query.SELECT.columns.forEach(column => {
|
|
119
148
|
if (column.ref && column.ref.length > 1) {
|
|
120
149
|
let refEntity = sourceEntity
|
|
121
150
|
let refAlias = sourceAlias
|
|
122
151
|
beforeCallback()
|
|
123
|
-
column.ref.forEach(
|
|
152
|
+
column.ref.forEach(ref => {
|
|
124
153
|
ref = ref.id || ref
|
|
125
154
|
// Alias
|
|
126
|
-
const matchAlias = refAlias.find(
|
|
155
|
+
const matchAlias = refAlias.find(alias => {
|
|
127
156
|
return alias.as === ref
|
|
128
157
|
})
|
|
129
158
|
let refElement
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const AUTH_ANNOTATIONS = [ '@restrict', '@requires' ]
|
|
2
4
|
|
|
3
5
|
module.exports = {
|
|
4
6
|
meta: {
|
|
@@ -6,12 +8,11 @@ module.exports = {
|
|
|
6
8
|
docs: {
|
|
7
9
|
description: '`@restrict` and `@requires` must not be empty.',
|
|
8
10
|
category: 'Model Validation',
|
|
9
|
-
recommended: true
|
|
11
|
+
recommended: true,
|
|
12
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-no-empty-restrictions',
|
|
10
13
|
},
|
|
11
|
-
hasSuggestions: true,
|
|
12
14
|
messages: {
|
|
13
|
-
|
|
14
|
-
ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
|
|
15
|
+
missingRestriction: 'No explicit restrictions provided on {{kind}} `{{name}}` at `{{label}}`.',
|
|
15
16
|
},
|
|
16
17
|
type: 'problem',
|
|
17
18
|
model: 'inferred'
|
|
@@ -19,15 +20,17 @@ module.exports = {
|
|
|
19
20
|
create (context) {
|
|
20
21
|
return {
|
|
21
22
|
entity: checkRestrictions,
|
|
22
|
-
service: checkRestrictions
|
|
23
|
+
service: checkRestrictions,
|
|
24
|
+
action: checkRestrictions,
|
|
25
|
+
function: checkRestrictions,
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function checkRestrictions (e) {
|
|
26
|
-
for (const
|
|
27
|
-
|
|
28
|
-
if (invalid) {
|
|
29
|
+
for (const anno of AUTH_ANNOTATIONS) {
|
|
30
|
+
if (isEmptyRestriction(e[anno])) {
|
|
29
31
|
context.report({
|
|
30
|
-
|
|
32
|
+
messageId: 'missingRestriction',
|
|
33
|
+
data: { kind: e.kind, name: e.name, label: anno },
|
|
31
34
|
node: context.getNode(e),
|
|
32
35
|
file: e.$location.file
|
|
33
36
|
})
|
|
@@ -36,3 +39,28 @@ module.exports = {
|
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if the given annotation value is an empty restriction.
|
|
45
|
+
* Examples which would return `true`:
|
|
46
|
+
* ```
|
|
47
|
+
* @requires: ''
|
|
48
|
+
* @requires: ['']
|
|
49
|
+
* @restrict: [ { to: '' } ]
|
|
50
|
+
* @restrict: [ { to: [''] } ]
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @param {*} obj
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function isEmptyRestriction(obj) {
|
|
57
|
+
if (typeof obj === 'string')
|
|
58
|
+
return obj === ''
|
|
59
|
+
if (Array.isArray(obj))
|
|
60
|
+
return obj.length === 0 || obj.some(isEmptyRestriction)
|
|
61
|
+
if (typeof obj === 'object') {
|
|
62
|
+
// handle `null` as non-empty (i.e. ignore)
|
|
63
|
+
return obj && isEmptyRestriction(obj.to)
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
schema: [{/* to avoid deprecation warning for ESLint 9 */ }],
|
|
4
6
|
docs: {
|
|
5
7
|
description: '`@restrict.grant` on service level and for bound/unbound actions and functions is limited to grant: \'*\'',
|
|
6
8
|
category: 'Model Validation',
|
|
7
|
-
recommended: true
|
|
9
|
+
recommended: true,
|
|
10
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-restrict-grant-service',
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
limitedGrant: `The grant value provided in @restrict is limited to '*' for {{kind}} '{{name}}'`,
|
|
8
14
|
},
|
|
9
15
|
type: 'problem',
|
|
10
16
|
model: 'inferred'
|
|
@@ -16,36 +22,30 @@ module.exports = {
|
|
|
16
22
|
service: checkRestrictGrant
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
function checkRestrictGrant
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
message,
|
|
41
|
-
node,
|
|
42
|
-
file
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
break
|
|
46
|
-
}
|
|
25
|
+
function checkRestrictGrant(def) {
|
|
26
|
+
if (!Array.isArray(def['@restrict']))
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
const node = context.getNode(def)
|
|
30
|
+
const file = def.$location.file
|
|
31
|
+
const data = { kind: def.kind, name: def.name }
|
|
32
|
+
|
|
33
|
+
for (const entry of def['@restrict']) {
|
|
34
|
+
if (entry?.grant !== undefined) {
|
|
35
|
+
|
|
36
|
+
if (typeof entry.grant === 'string') {
|
|
37
|
+
if (entry.grant !== '*')
|
|
38
|
+
context.report({ messageId: 'limitedGrant', data, node, file })
|
|
39
|
+
|
|
40
|
+
} else if (Array.isArray(entry.grant)) {
|
|
41
|
+
if (entry.grant.length === 0 || !entry.grant.some(val => val === '*'))
|
|
42
|
+
context.report({ messageId: 'limitedGrant', data, node, file })
|
|
43
|
+
|
|
44
|
+
} else {
|
|
45
|
+
// invalid grant value; ignored by this rule
|
|
47
46
|
}
|
|
48
47
|
}
|
|
48
|
+
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
@@ -5,12 +7,11 @@ module.exports = {
|
|
|
5
7
|
description: 'Use `@requires` instead of `@restrict.to` in actions and services with unrestricted events.',
|
|
6
8
|
category: 'Model Validation',
|
|
7
9
|
recommended: true,
|
|
8
|
-
version: '2.4.1'
|
|
10
|
+
version: '2.4.1',
|
|
11
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-use-requires',
|
|
9
12
|
},
|
|
10
|
-
hasSuggestions: true,
|
|
11
13
|
messages: {
|
|
12
|
-
|
|
13
|
-
ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
|
|
14
|
+
useRequires: 'Use `@requires` instead of `@restrict.to` at {{kind}} `{{name}}`.'
|
|
14
15
|
},
|
|
15
16
|
type: 'problem',
|
|
16
17
|
model: 'inferred'
|
|
@@ -18,22 +19,33 @@ module.exports = {
|
|
|
18
19
|
create (context) {
|
|
19
20
|
return {
|
|
20
21
|
service: checkRestrict,
|
|
21
|
-
action: checkRestrict
|
|
22
|
+
action: checkRestrict,
|
|
23
|
+
function: checkRestrict,
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function checkRestrict (e) {
|
|
25
|
-
if (e
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
if (!Array.isArray(e?.['@restrict']))
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
for (const entry of e['@restrict']) {
|
|
31
|
+
// Scenario: `@restrict: [ { to: 'Foo', grant: '*' } ]`
|
|
32
|
+
// There must be no `where` condition, as otherwise it wouldn't be equivalent
|
|
33
|
+
// to `@requires`. `to` must not be `null`, as `@requires: null` is not the
|
|
34
|
+
// same as `@restrict: [{to:null}]`.
|
|
35
|
+
// See https://cap.cloud.sap/docs/guides/security/authorization#supported-combinations-with-cds-resources
|
|
36
|
+
// for documentation.
|
|
37
|
+
if (entry?.to !== undefined && entry.to !== null &&
|
|
38
|
+
(entry.grant === '*' || !entry.grant) && entry.where === undefined) {
|
|
39
|
+
context.report({
|
|
40
|
+
messageId: 'useRequires',
|
|
41
|
+
data: { kind: e.kind, name: e.name },
|
|
42
|
+
node: context.getNode(e),
|
|
43
|
+
file: e.$location.file
|
|
44
|
+
})
|
|
45
|
+
break // max one report per annotation
|
|
35
46
|
}
|
|
36
47
|
}
|
|
37
48
|
}
|
|
49
|
+
|
|
38
50
|
}
|
|
39
51
|
}
|