@sap/eslint-plugin-cds 3.0.5 → 3.1.0
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 +32 -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 +14 -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 +36 -10
- package/lib/rules/auth-restrict-grant-service.js +19 -20
- package/lib/rules/auth-use-requires.js +25 -15
- package/lib/rules/auth-valid-restrict-grant.js +137 -81
- package/lib/rules/auth-valid-restrict-keys.js +34 -18
- package/lib/rules/auth-valid-restrict-to.js +67 -60
- package/lib/rules/auth-valid-restrict-where.js +31 -44
- 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 +15 -7
- 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 +13 -7
- package/lib/utils/runRuleTester.js +69 -36
- package/package.json +1 -1
- package/lib/utils/genDocs.js +0 -346
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { findFuzzy, isEmptyString, isEmptyObject, isStringInArray } = require('../utils/rules')
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
// See https://cap.cloud.sap/docs/guides/security/authorization#restrict-annotation
|
|
6
|
+
// The combination of these events is equivalent to the virtual 'WRITE' event.
|
|
7
|
+
const SAME_AS_WRITE_EVENT = [ 'CREATE', 'DELETE', 'UPDATE', 'UPSERT' ]
|
|
8
|
+
// Note that 'INSERT' is not meant to be used by users. They should use 'CREATE' instead.
|
|
9
|
+
const VALID_EVENTS = [ ...SAME_AS_WRITE_EVENT, 'READ', 'INSERT', '*', 'WRITE']
|
|
10
|
+
|
|
11
|
+
const TYPICAL_ISSUES = {
|
|
12
|
+
__proto__: null,
|
|
13
|
+
any: '*'
|
|
14
|
+
}
|
|
5
15
|
|
|
6
16
|
module.exports = {
|
|
7
17
|
meta: {
|
|
@@ -9,99 +19,145 @@ module.exports = {
|
|
|
9
19
|
docs: {
|
|
10
20
|
description: '`@restrict.grant` must have valid values.',
|
|
11
21
|
category: 'Model Validation',
|
|
12
|
-
recommended: true
|
|
22
|
+
recommended: true,
|
|
23
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-grant',
|
|
13
24
|
},
|
|
14
25
|
messages: {
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
invalidType: 'Invalid type for grant value. Must either be string or array of strings.',
|
|
27
|
+
invalidSingleType: 'Grant value must be a string.',
|
|
28
|
+
misspelledItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
29
|
+
unknownItem: `Invalid item '{{invalid}}'. Valid: [ {{candidates}} ].`,
|
|
30
|
+
replaceBy: `Replace grant values '{{values}}' with '{{name}}'.`,
|
|
31
|
+
missingEventOrAction: "Missing event/action on '{{name}}' for `@restrict.grant`.",
|
|
32
|
+
star: 'Grant value \'*\' overrides all other grants. Replace by \'*\' only.'
|
|
17
33
|
},
|
|
18
34
|
type: 'problem',
|
|
19
35
|
model: 'inferred'
|
|
20
36
|
},
|
|
21
37
|
create (context) {
|
|
22
38
|
return {
|
|
23
|
-
|
|
39
|
+
any: checkRestrictGrant
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
function checkRestrictGrant
|
|
42
|
+
function checkRestrictGrant(e) {
|
|
43
|
+
if (!e?.['@restrict'] || !Array.isArray(e['@restrict']))
|
|
44
|
+
return
|
|
45
|
+
|
|
27
46
|
const node = context.getNode(e)
|
|
28
47
|
const file = e.$location.file
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
48
|
+
const actionNames = e.actions ? Object.keys(e.actions) : []
|
|
49
|
+
const validEventsAndActions = [ ...VALID_EVENTS, ...actionNames ]
|
|
50
|
+
|
|
51
|
+
for (const entry of e['@restrict']) {
|
|
52
|
+
if (entry?.grant !== undefined) {
|
|
53
|
+
if (!checkRestrictEntry(entry)) {
|
|
54
|
+
// max. one message per annotation, to avoid spamming the user
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check an entry of the `@restrict` array.
|
|
62
|
+
* Returns `true` if the value is valid, `false` otherwise.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} entry
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
function checkRestrictEntry( entry ) {
|
|
68
|
+
if (typeof entry.grant === 'string') {
|
|
69
|
+
return checkSingleGrantValue(entry.grant)
|
|
70
|
+
|
|
71
|
+
} else if (!Array.isArray(entry.grant)) {
|
|
72
|
+
// neither string nor array: report invalid type
|
|
73
|
+
context.report({ messageId: 'invalidType', node, file })
|
|
74
|
+
return false
|
|
75
|
+
|
|
76
|
+
} else {
|
|
77
|
+
if (entry.grant.length === 0) {
|
|
78
|
+
context.report({
|
|
79
|
+
messageId: 'missingEventOrAction',
|
|
80
|
+
data: { name: e.name }, node, file
|
|
81
|
+
})
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const value of entry.grant) {
|
|
86
|
+
if (!checkSingleGrantValue(value))
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If grant values contain '*', '*' only is enough. It overrides everything.
|
|
91
|
+
if (entry.grant.length > 1 && entry.grant.includes('*')) {
|
|
92
|
+
context.report({ messageId: 'star', node, file })
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If the given values include all of SAME_AS_WRITE_EVENT, the user can
|
|
97
|
+
// replace them with 'WRITE'.
|
|
98
|
+
const includesWrite = SAME_AS_WRITE_EVENT.every(value => entry.grant.includes(value))
|
|
99
|
+
if (includesWrite) {
|
|
100
|
+
context.report({
|
|
101
|
+
messageId: 'replaceBy',
|
|
102
|
+
data: { values: SAME_AS_WRITE_EVENT.join(', '), name: 'WRITE' },
|
|
103
|
+
node, file
|
|
104
|
+
})
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns `true` if the value is valid, `false` otherwise.
|
|
113
|
+
*
|
|
114
|
+
* @param grant
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
function checkSingleGrantValue(grant) {
|
|
118
|
+
if (typeof grant !== 'string') {
|
|
119
|
+
// single grant values must be strings
|
|
120
|
+
context.report({ messageId: 'invalidSingleType', node, file })
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
if (grant.trim() === '') {
|
|
124
|
+
context.report({
|
|
125
|
+
messageId: 'missingEventOrAction',
|
|
126
|
+
data: { name: e.name }, node, file,
|
|
127
|
+
})
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!validEventsAndActions.includes(grant)) {
|
|
132
|
+
const candidates = TYPICAL_ISSUES[grant]
|
|
133
|
+
? [ TYPICAL_ISSUES[grant] ]
|
|
134
|
+
: findFuzzy(grant, validEventsAndActions.sort(), null, false, 2)
|
|
135
|
+
|
|
136
|
+
if (candidates.length === 0) {
|
|
137
|
+
context.report({
|
|
138
|
+
messageId: 'unknownItem',
|
|
139
|
+
data: { invalid: grant, candidates: validEventsAndActions.join(', ') }, node, file,
|
|
140
|
+
})
|
|
141
|
+
} else {
|
|
142
|
+
context.report({
|
|
143
|
+
messageId: 'misspelledItem',
|
|
144
|
+
data: { invalid: grant, candidates: candidates.join(', ') }, node, file,
|
|
145
|
+
})
|
|
103
146
|
}
|
|
147
|
+
return false
|
|
104
148
|
}
|
|
149
|
+
|
|
150
|
+
if (grant === 'INSERT') {
|
|
151
|
+
// special case: 'INSERT' is an internal event name. Users should use 'CREATE' instead.
|
|
152
|
+
context.report({
|
|
153
|
+
messageId: 'replaceBy',
|
|
154
|
+
data: { values: 'INSERT', name: 'CREATE' },
|
|
155
|
+
node, file
|
|
156
|
+
})
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return true
|
|
105
161
|
}
|
|
106
162
|
}
|
|
107
163
|
}
|
|
@@ -1,40 +1,56 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { isEmptyObject, findFuzzy } = require('../utils/rules')
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
// These are the only valid properties inside `@restrict` annotations.
|
|
6
|
+
// They are described in <https://cap.cloud.sap/docs/guides/security/authorization#restrict-annotation>
|
|
7
|
+
const VALID_RESTRICT_PROPERTIES = ['grant', 'to', 'where']
|
|
4
8
|
|
|
5
9
|
module.exports = {
|
|
6
10
|
meta: {
|
|
7
11
|
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
8
12
|
docs: {
|
|
9
|
-
description: '`@restrict` must have
|
|
13
|
+
description: '`@restrict` must not have properties besides `to`, `grant`, and `where`.',
|
|
10
14
|
category: 'Model Validation',
|
|
11
|
-
recommended: true
|
|
15
|
+
recommended: true,
|
|
16
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-keys',
|
|
12
17
|
},
|
|
13
18
|
messages: {
|
|
14
|
-
|
|
19
|
+
misspelledProperty: "Misspelled or unknown property '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
20
|
+
unknownProperty: "Unknown property '{{invalid}}'. Possible: ['to', 'grant', 'where']",
|
|
15
21
|
},
|
|
16
22
|
type: 'problem',
|
|
17
23
|
model: 'inferred'
|
|
18
24
|
},
|
|
19
25
|
create (context) {
|
|
20
26
|
return {
|
|
21
|
-
|
|
27
|
+
any: checkRestrictKeys
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
function checkRestrictKeys (e) {
|
|
25
|
-
if (e['@restrict'])
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
if (!Array.isArray(e?.['@restrict']))
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
for (const entry of e['@restrict']) {
|
|
35
|
+
if (typeof entry === 'object' && !isEmptyObject(entry)) {
|
|
36
|
+
for (const key of Object.keys(entry)) {
|
|
37
|
+
if (VALID_RESTRICT_PROPERTIES.includes(key))
|
|
38
|
+
continue
|
|
39
|
+
const candidates = findFuzzy(key, VALID_RESTRICT_PROPERTIES.sort(), null, false, 2)
|
|
40
|
+
if (candidates.length === 0) {
|
|
41
|
+
context.report({
|
|
42
|
+
messageId: 'unknownProperty',
|
|
43
|
+
data: { invalid: key },
|
|
44
|
+
node: context.getNode(e),
|
|
45
|
+
file: e.$location.file
|
|
46
|
+
})
|
|
47
|
+
} else {
|
|
48
|
+
context.report({
|
|
49
|
+
messageId: 'misspelledProperty',
|
|
50
|
+
data: { invalid: key, candidates },
|
|
51
|
+
node: context.getNode(e),
|
|
52
|
+
file: e.$location.file
|
|
53
|
+
})
|
|
38
54
|
}
|
|
39
55
|
}
|
|
40
56
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { isEmptyString, isStringInArray, findFuzzy, isEmptyObject } = require('../utils/rules')
|
|
2
4
|
|
|
3
5
|
const VALID_PSEUDO_ROLES = ['authenticated-user', 'system-user', 'any']
|
|
@@ -8,12 +10,13 @@ module.exports = {
|
|
|
8
10
|
docs: {
|
|
9
11
|
description: '`@restrict.to` must have valid values.',
|
|
10
12
|
category: 'Model Validation',
|
|
11
|
-
recommended: true
|
|
13
|
+
recommended: true,
|
|
14
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-to',
|
|
12
15
|
},
|
|
13
|
-
hasSuggestions: true,
|
|
14
16
|
messages: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
invalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
18
|
+
missingRole: "Missing role on '{{name}}' for `@restrict.to`.",
|
|
19
|
+
missingRoles: "Missing roles on '{{name}}' for `@restrict.to`."
|
|
17
20
|
},
|
|
18
21
|
type: 'problem',
|
|
19
22
|
model: 'inferred'
|
|
@@ -26,23 +29,25 @@ module.exports = {
|
|
|
26
29
|
function checkRestrictTo (e) {
|
|
27
30
|
const USER_ROLES = []
|
|
28
31
|
const model = context.getModel()
|
|
32
|
+
if (!model)
|
|
33
|
+
return
|
|
29
34
|
|
|
30
|
-
model.foreach('entity',
|
|
35
|
+
model.foreach('entity', e => {
|
|
31
36
|
if (e['@restrict']) {
|
|
32
|
-
e['@restrict'].forEach(
|
|
37
|
+
e['@restrict'].forEach(p => {
|
|
33
38
|
if (p.to) {
|
|
34
39
|
switch (typeof p.to) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
40
|
+
case 'string':
|
|
41
|
+
if (p.to !== p.to.toLowerCase() && !USER_ROLES.includes(p.to)) {
|
|
42
|
+
USER_ROLES.push(p.to)
|
|
43
|
+
}
|
|
44
|
+
break
|
|
45
|
+
case 'object':
|
|
46
|
+
for (const r in p.to) {
|
|
47
|
+
if (r !== r.toLowerCase() && !USER_ROLES.includes(r)) {
|
|
48
|
+
USER_ROLES.push(r)
|
|
45
49
|
}
|
|
50
|
+
}
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
})
|
|
@@ -71,60 +76,62 @@ module.exports = {
|
|
|
71
76
|
const toValue = entry.to
|
|
72
77
|
|
|
73
78
|
switch (typeof toValue) {
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
case 'string': {
|
|
80
|
+
if (isEmptyString(toValue)) {
|
|
81
|
+
context.report({
|
|
82
|
+
messageId: 'missingRole',
|
|
83
|
+
data: { name: e.name },
|
|
84
|
+
node,
|
|
85
|
+
file,
|
|
86
|
+
})
|
|
87
|
+
} else {
|
|
88
|
+
const isPseudoRole = entry.to && entry.to === entry.to.toLowerCase()
|
|
89
|
+
if (!isStringInArray(toValue, ROLES, isPseudoRole)) {
|
|
90
|
+
const candidates = findFuzzy(toValue, ROLES.sort())
|
|
76
91
|
context.report({
|
|
77
|
-
|
|
92
|
+
messageId: 'invalidItem',
|
|
93
|
+
data: { invalid: toValue, candidates },
|
|
78
94
|
node,
|
|
79
|
-
file
|
|
95
|
+
file,
|
|
80
96
|
})
|
|
81
|
-
} else {
|
|
82
|
-
const isPseudoRole = entry.to && entry.to === entry.to.toLowerCase()
|
|
83
|
-
if (!isStringInArray(toValue, ROLES, isPseudoRole)) {
|
|
84
|
-
const candidates = findFuzzy(toValue, ROLES.sort())
|
|
85
|
-
context.report({
|
|
86
|
-
messageId: 'InvalidItem',
|
|
87
|
-
data: { invalid: toValue, candidates },
|
|
88
|
-
node,
|
|
89
|
-
file
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
97
|
}
|
|
93
|
-
break
|
|
94
98
|
}
|
|
99
|
+
break
|
|
100
|
+
}
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
case 'object':
|
|
103
|
+
if (isEmptyObject(toValue)) {
|
|
104
|
+
context.report({
|
|
105
|
+
messageId: 'missingRoles',
|
|
106
|
+
data: { name: e.name },
|
|
107
|
+
node,
|
|
108
|
+
file,
|
|
109
|
+
})
|
|
110
|
+
} else {
|
|
111
|
+
// If values contain 'any', 'any' only is enough
|
|
112
|
+
if (toValue.length > 1 && toValue.includes('any')) {
|
|
98
113
|
context.report({
|
|
99
|
-
|
|
114
|
+
messageId: 'invalidItem',
|
|
115
|
+
data: { invalid: `[${toValue}]`, candidates: ['["any"]'] },
|
|
100
116
|
node,
|
|
101
|
-
file
|
|
102
|
-
})
|
|
103
|
-
} else {
|
|
104
|
-
// If values contain 'any', 'any' only is enough
|
|
105
|
-
if (toValue.length > 1 && toValue.includes('any')) {
|
|
106
|
-
context.report({
|
|
107
|
-
messageId: 'InvalidItem',
|
|
108
|
-
data: { invalid: `[${toValue}]`, candidates: ['["any"]'] },
|
|
109
|
-
node,
|
|
110
|
-
file
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
toValue.forEach((value) => {
|
|
114
|
-
if (!ROLES.includes(value)) {
|
|
115
|
-
const candidates = findFuzzy(value, ROLES.sort(), undefined, false, 2)
|
|
116
|
-
if (candidates.length > 0) {
|
|
117
|
-
context.report({
|
|
118
|
-
messageId: 'InvalidItem',
|
|
119
|
-
data: { invalid: value, candidates },
|
|
120
|
-
node,
|
|
121
|
-
file
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
}
|
|
117
|
+
file,
|
|
125
118
|
})
|
|
126
119
|
}
|
|
127
|
-
|
|
120
|
+
toValue.forEach(value => {
|
|
121
|
+
if (!ROLES.includes(value)) {
|
|
122
|
+
const candidates = findFuzzy(value, ROLES.sort(), undefined, false, 2)
|
|
123
|
+
if (candidates.length > 0) {
|
|
124
|
+
context.report({
|
|
125
|
+
messageId: 'invalidItem',
|
|
126
|
+
data: { invalid: value, candidates },
|
|
127
|
+
node,
|
|
128
|
+
file,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
break
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const cds = require('@sap/cds')
|
|
2
4
|
|
|
3
5
|
module.exports = {
|
|
@@ -6,65 +8,50 @@ module.exports = {
|
|
|
6
8
|
docs: {
|
|
7
9
|
description: '`@restrict.where` must have valid values.',
|
|
8
10
|
category: 'Model Validation',
|
|
9
|
-
recommended: true
|
|
11
|
+
recommended: true,
|
|
12
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-where',
|
|
10
13
|
},
|
|
11
14
|
severity: 'error',
|
|
12
|
-
hasSuggestions: true,
|
|
13
15
|
messages: {
|
|
14
|
-
|
|
15
|
-
ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
|
|
16
|
+
CompilationFailed: 'Invalid `where` expression, CDS compilation failed.',
|
|
16
17
|
},
|
|
17
18
|
type: 'problem',
|
|
18
19
|
model: 'inferred'
|
|
19
20
|
},
|
|
20
21
|
create (context) {
|
|
21
22
|
const model = context.getModel()
|
|
23
|
+
if (!model)
|
|
24
|
+
return
|
|
22
25
|
|
|
23
|
-
return {
|
|
24
|
-
entity
|
|
26
|
+
return function checkRestrictWhere() {
|
|
27
|
+
model.foreach('entity', checkEntityRestrict)
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
function
|
|
28
|
-
|
|
30
|
+
function checkEntityRestrict (def) {
|
|
31
|
+
if (!Array.isArray(def['@restrict']))
|
|
32
|
+
return
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
switch (typeof p.to) {
|
|
35
|
-
case 'string':
|
|
36
|
-
if (p.to !== p.to.toLowerCase() && !USER_ROLES.includes(p.to)) {
|
|
37
|
-
USER_ROLES.push(p.to)
|
|
38
|
-
}
|
|
39
|
-
break
|
|
40
|
-
case 'object':
|
|
41
|
-
for (const r in p.to) {
|
|
42
|
-
if (r !== r.toLowerCase() && !USER_ROLES.includes(r)) {
|
|
43
|
-
USER_ROLES.push(r)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
})
|
|
34
|
+
for (const entry of def['@restrict']) {
|
|
35
|
+
if (entry.where) {
|
|
36
|
+
// TODO: Check return value (where xpr)
|
|
37
|
+
checkAndCompileWhereExpression(entry.where)
|
|
49
38
|
}
|
|
50
|
-
}
|
|
39
|
+
}
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}
|
|
41
|
+
function checkAndCompileWhereExpression(where) {
|
|
42
|
+
try {
|
|
43
|
+
const compileOptions = {}
|
|
44
|
+
return typeof where === 'string'
|
|
45
|
+
? cds.parse.expr(where, compileOptions)
|
|
46
|
+
: where
|
|
47
|
+
|
|
48
|
+
} catch {
|
|
49
|
+
context.report({
|
|
50
|
+
messageId: 'CompilationFailed',
|
|
51
|
+
node: context.getNode(def),
|
|
52
|
+
file: def.$location.file,
|
|
53
|
+
})
|
|
54
|
+
return null
|
|
68
55
|
}
|
|
69
56
|
}
|
|
70
57
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const cds = requireMtx()
|
|
2
4
|
|
|
3
|
-
const { dirname } = require('path')
|
|
5
|
+
const { dirname } = require('node:path')
|
|
4
6
|
|
|
5
7
|
const rule = module.exports = {
|
|
6
8
|
meta: {
|
|
7
9
|
docs: {
|
|
8
10
|
description: 'Extensions must not violate restrictions set by the extended SaaS app.',
|
|
9
11
|
category: 'Model Validation',
|
|
10
|
-
recommended: true
|
|
12
|
+
recommended: true,
|
|
13
|
+
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/extension-restrictions',
|
|
11
14
|
},
|
|
12
15
|
hasSuggestions: false,
|
|
13
16
|
type: 'problem',
|
|
@@ -41,6 +44,9 @@ const rule = module.exports = {
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @param {CDSRuleContext} context
|
|
49
|
+
*/
|
|
44
50
|
function baseModel (context) {
|
|
45
51
|
let dir = context.getFilename()
|
|
46
52
|
do {
|
|
@@ -61,9 +67,11 @@ function baseModel (context) {
|
|
|
61
67
|
function requireMtx () {
|
|
62
68
|
const cds = require('@sap/cds')
|
|
63
69
|
try {
|
|
70
|
+
// eslint-disable-next-line
|
|
64
71
|
const pkg = require.resolve('@sap/cds-mtxs', { paths: [cds.root, __dirname] })
|
|
65
72
|
return require(pkg)
|
|
66
73
|
} catch (e) {
|
|
67
|
-
if (e.code !== 'MODULE_NOT_FOUND')
|
|
74
|
+
if (e.code !== 'MODULE_NOT_FOUND')
|
|
75
|
+
throw e
|
|
68
76
|
}
|
|
69
77
|
}
|
package/lib/rules/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const Cache = require('../utils/Cache')
|
|
2
|
-
const
|
|
4
|
+
const createRule = require('../utils/createRule')
|
|
3
5
|
|
|
4
6
|
const rules = {
|
|
5
7
|
'assoc2many-ambiguous-key': () => createRule(require('./assoc2many-ambiguous-key')),
|
|
@@ -12,8 +14,10 @@ const rules = {
|
|
|
12
14
|
'auth-valid-restrict-where': () => createRule(require('./auth-valid-restrict-where')),
|
|
13
15
|
'latest-cds-version': () => createRule(require('./latest-cds-version')),
|
|
14
16
|
'no-db-keywords': () => createRule(require('./no-db-keywords')),
|
|
17
|
+
'no-java-keywords': () => createRule(require('./no-java-keywords')),
|
|
15
18
|
'no-dollar-prefixed-names': () => createRule(require('./no-dollar-prefixed-names')),
|
|
16
19
|
'no-join-on-draft': () => createRule(require('./no-join-on-draft')),
|
|
20
|
+
'sql-null-comparison': () => createRule(require('./sql-null-comparison')),
|
|
17
21
|
'sql-cast-suggestion': () => createRule(require('./sql-cast-suggestion')),
|
|
18
22
|
'start-elements-lowercase': () => createRule(require('./start-elements-lowercase')),
|
|
19
23
|
'start-entities-uppercase': () => createRule(require('./start-entities-uppercase')),
|