@cap-js/ord 1.7.0 → 1.9.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/README.md +10 -5
- package/lib/auth/authentication.js +30 -33
- package/lib/auth/cf-mtls.js +5 -5
- package/lib/build.js +1 -3
- package/lib/common/placeholders.js +6 -0
- package/lib/common/slice.js +72 -0
- package/lib/common/utils.js +190 -0
- package/lib/configuration.js +33 -88
- package/lib/constants.js +2 -32
- package/lib/defaults.js +27 -139
- package/lib/extend-ord-with-custom.js +82 -51
- package/lib/interop-csn.js +23 -11
- package/lib/logger.js +1 -3
- package/lib/meta-data.js +14 -26
- package/lib/ord.js +55 -123
- package/lib/protocol-resolver.js +26 -96
- package/lib/services/ord-service.js +30 -6
- package/lib/templates/api-resource.js +283 -0
- package/lib/templates/entity-type.js +121 -0
- package/lib/templates/event-resource.js +161 -0
- package/lib/templates/group.js +50 -0
- package/lib/templates/integration-dependency.js +98 -0
- package/lib/templates/package.js +91 -0
- package/lib/templates/product.js +57 -0
- package/package.json +7 -5
- package/data/well-known.json +0 -14
- package/lib/access-strategies.js +0 -172
- package/lib/date.js +0 -17
- package/lib/integration-dependency.js +0 -125
- package/lib/templates.js +0 -613
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ This will output something like `admin:$2y$05$...` - use only the hash part (sta
|
|
|
114
114
|
|
|
115
115
|
#### CF mTLS Authentication
|
|
116
116
|
|
|
117
|
-
Configure Cloud Foundry mutual TLS authentication for SAP BTP Cloud Foundry environments.
|
|
117
|
+
Configure Cloud Foundry mutual TLS authentication for SAP BTP Cloud Foundry environments. Possible values include 'sap:cmp-mtls:v1' and 'sap.businesshub:mtls:v1'.
|
|
118
118
|
|
|
119
119
|
**Production Configuration with UCL (Recommended)**
|
|
120
120
|
|
|
@@ -131,9 +131,11 @@ For SAP UCL (Unified Customer Landscape) integration, enable mTLS in `.cdsrc.jso
|
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
|
+
# Example configuration of the CF_MTLS_TRUSTED_CERTS environment variable
|
|
134
135
|
export CF_MTLS_TRUSTED_CERTS='{
|
|
135
136
|
"configEndpoints": ["https://your-ucl-endpoint/v1/info"],
|
|
136
|
-
"rootCaDn": ["CN=SAP Cloud Root CA,O=SAP SE,L=Walldorf,C=DE"]
|
|
137
|
+
"rootCaDn": ["CN=SAP Cloud Root CA,O=SAP SE,L=Walldorf,C=DE"],
|
|
138
|
+
"accessStrategies": ["sap:cmp-mtls:v1", "sap.businesshub:mtls:v1"]
|
|
137
139
|
}'
|
|
138
140
|
```
|
|
139
141
|
|
|
@@ -142,9 +144,11 @@ export CF_MTLS_TRUSTED_CERTS='{
|
|
|
142
144
|
For custom certificates without UCL:
|
|
143
145
|
|
|
144
146
|
```bash
|
|
147
|
+
# Example configuration of the CF_MTLS_TRUSTED_CERTS environment variable
|
|
145
148
|
export CF_MTLS_TRUSTED_CERTS='{
|
|
146
149
|
"certs": [{"issuer": "CN=My CA,O=MyOrg", "subject": "CN=my-service,O=MyOrg"}],
|
|
147
|
-
"rootCaDn": ["CN=My Root CA,O=MyOrg"]
|
|
150
|
+
"rootCaDn": ["CN=My Root CA,O=MyOrg"],
|
|
151
|
+
"accessStrategies": ["sap:cmp-mtls:v1", "sap.businesshub:mtls:v1"]
|
|
148
152
|
}'
|
|
149
153
|
```
|
|
150
154
|
|
|
@@ -157,13 +161,14 @@ For local development, configure the full mTLS settings directly in `.cdsrc.json
|
|
|
157
161
|
"ord": {
|
|
158
162
|
"authentication": {
|
|
159
163
|
"cfMtls": {
|
|
164
|
+
"rootCaDn": ["CN=Test Root CA,O=MyOrg,C=DE"],
|
|
165
|
+
"accessStrategies": ["sap:cmp-mtls:v1","sap.businesshub:mtls:v1"],
|
|
160
166
|
"certs": [
|
|
161
167
|
{
|
|
162
168
|
"issuer": "CN=Test CA,O=MyOrg,C=DE",
|
|
163
169
|
"subject": "CN=test-client,O=MyOrg,C=DE"
|
|
164
170
|
}
|
|
165
|
-
]
|
|
166
|
-
"rootCaDn": ["CN=Test Root CA,O=MyOrg,C=DE"]
|
|
171
|
+
]
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
}
|
|
@@ -23,9 +23,8 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
const cds = require("@sap/cds");
|
|
26
|
-
const {
|
|
26
|
+
const { ORD_ACCESS_STRATEGY, BASIC_AUTH_HEADER_KEY, AUTH_STRINGS, CF_MTLS_HEADERS } = require("../constants");
|
|
27
27
|
const Logger = require("../logger");
|
|
28
|
-
const { getAccessStrategiesFromAuthConfig } = require("../access-strategies");
|
|
29
28
|
const bcrypt = require("bcryptjs");
|
|
30
29
|
const { createCfMtlsConfig, handleCfMtlsAuthentication } = require("./cf-mtls");
|
|
31
30
|
|
|
@@ -102,10 +101,10 @@ async function ensureCfMtlsValidator(authConfig) {
|
|
|
102
101
|
* 1. Environment variables (BASIC_AUTH, CF_MTLS_TRUSTED_CERTS) - for production deployments
|
|
103
102
|
* 2. .cdsrc.json settings (cds.env.ord.authentication.basic, cds.env.ord.authentication.cfMtls) - for development and testing
|
|
104
103
|
*
|
|
105
|
-
*
|
|
106
|
-
* - If cds.env.ord.authentication.basic exists →
|
|
104
|
+
* Access strategies are automatically detected based on the presence of configuration:
|
|
105
|
+
* - If cds.env.ord.authentication.basic exists → 'basic-auth' is enabled
|
|
107
106
|
* - If cds.env.ord.authentication.cfMtls exists → CF mTLS authentication enabled
|
|
108
|
-
* - Multiple
|
|
107
|
+
* - Multiple access strategies can coexist and are tried in order
|
|
109
108
|
* - Open authentication is the default when no secure authentication is configured
|
|
110
109
|
*
|
|
111
110
|
* This approach follows the 12-Factor App principles where environment variables
|
|
@@ -118,12 +117,11 @@ async function ensureCfMtlsValidator(authConfig) {
|
|
|
118
117
|
*/
|
|
119
118
|
function createAuthConfig() {
|
|
120
119
|
const defaultAuthConfig = {
|
|
121
|
-
|
|
122
|
-
accessStrategies: [{ type: AUTHENTICATION_TYPE.Open }],
|
|
120
|
+
accessStrategies: [ORD_ACCESS_STRATEGY.Open],
|
|
123
121
|
};
|
|
124
122
|
|
|
125
123
|
try {
|
|
126
|
-
const authConfig = {
|
|
124
|
+
const authConfig = { accessStrategies: [] };
|
|
127
125
|
const ordAuth = cds.env.ord?.authentication || {};
|
|
128
126
|
|
|
129
127
|
// Detect Basic authentication by checking for credentials
|
|
@@ -145,30 +143,30 @@ function createAuthConfig() {
|
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
|
|
148
|
-
authConfig.
|
|
146
|
+
authConfig.accessStrategies.push(ORD_ACCESS_STRATEGY.Basic);
|
|
149
147
|
authConfig.credentials = credentials;
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
// Detect CF mTLS authentication by checking for cfMtls config
|
|
153
151
|
if (process.env.CF_MTLS_TRUSTED_CERTS || ordAuth.cfMtls) {
|
|
154
|
-
|
|
152
|
+
const { accessStrategies } = JSON.parse(
|
|
153
|
+
process.env.CF_MTLS_TRUSTED_CERTS ??
|
|
154
|
+
JSON.stringify(typeof ordAuth.cfMtls === "object" ? ordAuth.cfMtls : {}),
|
|
155
|
+
);
|
|
156
|
+
|
|
155
157
|
// Mark for lazy initialization - validator will be loaded on first use
|
|
156
158
|
authConfig.cfMtlsValidator = null;
|
|
157
159
|
authConfig._cfMtlsInitPromise = null;
|
|
160
|
+
authConfig.accessStrategies.push(...(accessStrategies ?? [ORD_ACCESS_STRATEGY.CmpMtls]));
|
|
158
161
|
}
|
|
159
162
|
|
|
160
|
-
// If no
|
|
161
|
-
if (authConfig.
|
|
163
|
+
// If no access strategies detected, default to Open
|
|
164
|
+
if (authConfig.accessStrategies.length === 0) {
|
|
162
165
|
Logger.info("createAuthConfig:", 'No authentication configured. Defaulting to "Open" authentication');
|
|
163
166
|
return defaultAuthConfig;
|
|
164
167
|
}
|
|
165
168
|
|
|
166
|
-
|
|
167
|
-
// This ensures consistent mapping between auth types and ORD access strategies
|
|
168
|
-
// All mapping logic is centralized in lib/access-strategies.js
|
|
169
|
-
authConfig.accessStrategies = getAccessStrategiesFromAuthConfig(authConfig);
|
|
170
|
-
|
|
171
|
-
Logger.info("createAuthConfig:", `Configured authentication types: ${authConfig.types.join(", ")}`);
|
|
169
|
+
Logger.info("createAuthConfig:", `Configured access strategies: ${authConfig.accessStrategies.join(", ")}`);
|
|
172
170
|
return authConfig;
|
|
173
171
|
} catch (error) {
|
|
174
172
|
Logger.error("createAuthConfig:", `Configuration error: ${error.message}`);
|
|
@@ -264,9 +262,10 @@ async function openAuthStrategy() {
|
|
|
264
262
|
* Strategy registry mapping authentication types to their handlers
|
|
265
263
|
*/
|
|
266
264
|
const AUTH_STRATEGIES = {
|
|
267
|
-
[
|
|
268
|
-
[
|
|
269
|
-
[
|
|
265
|
+
[ORD_ACCESS_STRATEGY.Open]: openAuthStrategy,
|
|
266
|
+
[ORD_ACCESS_STRATEGY.Basic]: basicAuthStrategy,
|
|
267
|
+
[ORD_ACCESS_STRATEGY.CmpMtls]: cfMtlsAuthStrategy,
|
|
268
|
+
[ORD_ACCESS_STRATEGY.BahMtls]: cfMtlsAuthStrategy,
|
|
270
269
|
};
|
|
271
270
|
|
|
272
271
|
/**
|
|
@@ -279,13 +278,13 @@ const AUTH_STRATEGIES = {
|
|
|
279
278
|
function createAuthMiddleware(authConfig) {
|
|
280
279
|
return async function authenticate(req, res, next) {
|
|
281
280
|
// Handle invalid configuration
|
|
282
|
-
if (!authConfig || !authConfig.
|
|
281
|
+
if (!authConfig || !authConfig.accessStrategies || !Array.isArray(authConfig.accessStrategies)) {
|
|
283
282
|
Logger.error("Invalid auth configuration:", authConfig);
|
|
284
283
|
return res.status(401).send("Not authorized");
|
|
285
284
|
}
|
|
286
285
|
|
|
287
286
|
// If open authentication, allow immediately
|
|
288
|
-
if (authConfig.
|
|
287
|
+
if (authConfig.accessStrategies.includes(ORD_ACCESS_STRATEGY.Open)) {
|
|
289
288
|
res.status(200);
|
|
290
289
|
return next();
|
|
291
290
|
}
|
|
@@ -293,17 +292,15 @@ function createAuthMiddleware(authConfig) {
|
|
|
293
292
|
// Try each registered authentication strategy
|
|
294
293
|
const results = [];
|
|
295
294
|
|
|
296
|
-
for (const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (!strategy) {
|
|
300
|
-
Logger.warn(`Unknown authentication type: ${authType}`);
|
|
295
|
+
for (const strategy of authConfig.accessStrategies) {
|
|
296
|
+
if (!Object.values(ORD_ACCESS_STRATEGY).includes(strategy)) {
|
|
297
|
+
Logger.warn(`Unknown access strategy: ${strategy}`);
|
|
301
298
|
continue;
|
|
302
299
|
}
|
|
303
300
|
|
|
304
301
|
try {
|
|
305
|
-
const result = await strategy(req, res, authConfig);
|
|
306
|
-
results.push({ type:
|
|
302
|
+
const result = await AUTH_STRATEGIES[strategy](req, res, authConfig);
|
|
303
|
+
results.push({ type: strategy, ...result });
|
|
307
304
|
|
|
308
305
|
if (result.success) {
|
|
309
306
|
res.status(200);
|
|
@@ -315,8 +312,8 @@ function createAuthMiddleware(authConfig) {
|
|
|
315
312
|
return;
|
|
316
313
|
}
|
|
317
314
|
} catch (error) {
|
|
318
|
-
Logger.error(`Error in ${
|
|
319
|
-
results.push({ type:
|
|
315
|
+
Logger.error(`Error in ${strategy} authentication:`, error.message);
|
|
316
|
+
results.push({ type: strategy, success: false, handled: true, error: error.message });
|
|
320
317
|
}
|
|
321
318
|
}
|
|
322
319
|
|
|
@@ -328,7 +325,7 @@ function createAuthMiddleware(authConfig) {
|
|
|
328
325
|
// No authentication method was attempted
|
|
329
326
|
const wwwAuthHeaders = [];
|
|
330
327
|
|
|
331
|
-
if (authConfig.
|
|
328
|
+
if (authConfig.accessStrategies.includes(ORD_ACCESS_STRATEGY.Basic)) {
|
|
332
329
|
wwwAuthHeaders.push(AUTH_STRINGS.WWW_AUTHENTICATE_REALM);
|
|
333
330
|
}
|
|
334
331
|
|
package/lib/auth/cf-mtls.js
CHANGED
|
@@ -341,12 +341,12 @@ async function createCfMtlsConfig(cds, Logger) {
|
|
|
341
341
|
* @param {Object} res - Express response object
|
|
342
342
|
* @param {Object} authConfig - Authentication configuration
|
|
343
343
|
* @param {Function} authConfig.cfMtlsValidator - CF mTLS validator function
|
|
344
|
-
* @param {Array} authConfig.
|
|
344
|
+
* @param {Array<string>} authConfig.accessStrategies - Array of enabled access strategies
|
|
345
345
|
* @param {Object} Logger - Logger instance for error messages
|
|
346
346
|
* @returns {Object} Result object with success flag and optional next() call
|
|
347
347
|
*/
|
|
348
348
|
function handleCfMtlsAuthentication(req, res, authConfig, Logger) {
|
|
349
|
-
const {
|
|
349
|
+
const { ORD_ACCESS_STRATEGY, CF_MTLS_ERROR_REASON, AUTH_STRINGS } = require("../constants");
|
|
350
350
|
const result = authConfig.cfMtlsValidator(req);
|
|
351
351
|
|
|
352
352
|
if (result.ok) {
|
|
@@ -361,7 +361,7 @@ function handleCfMtlsAuthentication(req, res, authConfig, Logger) {
|
|
|
361
361
|
if (result.reason === CF_MTLS_ERROR_REASON.XFCC_VERIFICATION_FAILED) {
|
|
362
362
|
Logger.error("CF mTLS authentication failed: Missing proxy verification");
|
|
363
363
|
// If Basic auth is also configured, provide fallback
|
|
364
|
-
if (authConfig.
|
|
364
|
+
if (authConfig.accessStrategies.includes(ORD_ACCESS_STRATEGY.Basic)) {
|
|
365
365
|
res.status(401)
|
|
366
366
|
.setHeader("WWW-Authenticate", AUTH_STRINGS.WWW_AUTHENTICATE_REALM)
|
|
367
367
|
.send("Authentication required.");
|
|
@@ -373,7 +373,7 @@ function handleCfMtlsAuthentication(req, res, authConfig, Logger) {
|
|
|
373
373
|
|
|
374
374
|
if (result.reason === CF_MTLS_ERROR_REASON.NO_HEADERS) {
|
|
375
375
|
// If Basic auth is also configured, provide a more informative message
|
|
376
|
-
if (authConfig.
|
|
376
|
+
if (authConfig.accessStrategies.includes(ORD_ACCESS_STRATEGY.Basic)) {
|
|
377
377
|
res.status(401)
|
|
378
378
|
.setHeader("WWW-Authenticate", AUTH_STRINGS.WWW_AUTHENTICATE_REALM)
|
|
379
379
|
.send("Authentication required.");
|
|
@@ -386,7 +386,7 @@ function handleCfMtlsAuthentication(req, res, authConfig, Logger) {
|
|
|
386
386
|
if (result.reason === CF_MTLS_ERROR_REASON.HEADER_MISSING) {
|
|
387
387
|
Logger.error(`CF mTLS authentication failed: Missing header ${result.missing}`);
|
|
388
388
|
// If Basic auth is also configured, provide fallback
|
|
389
|
-
if (authConfig.
|
|
389
|
+
if (authConfig.accessStrategies.includes(ORD_ACCESS_STRATEGY.Basic)) {
|
|
390
390
|
res.status(401)
|
|
391
391
|
.setHeader("WWW-Authenticate", AUTH_STRINGS.WWW_AUTHENTICATE_REALM)
|
|
392
392
|
.send("Authentication required.");
|
package/lib/build.js
CHANGED
|
@@ -58,9 +58,7 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
_createRelativePath(url) {
|
|
61
|
-
|
|
62
|
-
if (relative.startsWith("/")) relative = relative.slice(1);
|
|
63
|
-
return path.join(...relative.replace(/:/g, "_").split("/"));
|
|
61
|
+
return url.replace(/^\/ord\/v1\//, "").replace(/:/g, "_");
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
_createWorkerPool(tasks, model) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const ordMainPartKeys = Object.freeze([
|
|
2
|
+
"groups",
|
|
3
|
+
"$schema",
|
|
4
|
+
"packages",
|
|
5
|
+
"products",
|
|
6
|
+
"groupTypes",
|
|
7
|
+
"description",
|
|
8
|
+
"perspective",
|
|
9
|
+
"policyLevel",
|
|
10
|
+
"policyLevels",
|
|
11
|
+
"customPolicyLevel",
|
|
12
|
+
"consumptionBundles",
|
|
13
|
+
"describedSystemType",
|
|
14
|
+
"openResourceDiscovery",
|
|
15
|
+
"describedSystemVersion",
|
|
16
|
+
"describedSystemInstance",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function getFileSizeInBytes(document) {
|
|
20
|
+
return Buffer.byteLength(JSON.stringify(document), "utf8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function slice(document, limit, chunk = 100) {
|
|
24
|
+
if (getFileSizeInBytes(document) < limit) {
|
|
25
|
+
return [document];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const slices = [];
|
|
29
|
+
const ord = Object.fromEntries(
|
|
30
|
+
Object.entries(document) //
|
|
31
|
+
.filter(([key, value]) => ordMainPartKeys.includes(key) || !Array.isArray(value) || !value.length),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
let slice = {};
|
|
35
|
+
let sliceSize = 2; // getFileSizeInBytes({});
|
|
36
|
+
const adjustedLimit = limit - getFileSizeInBytes(ord);
|
|
37
|
+
|
|
38
|
+
Object.entries(document)
|
|
39
|
+
.filter(([key]) => !(key in ord))
|
|
40
|
+
.forEach(([key, value]) => {
|
|
41
|
+
let start = 0;
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < value.length; i += chunk) {
|
|
44
|
+
const remaining = adjustedLimit - sliceSize;
|
|
45
|
+
const additional = getFileSizeInBytes({
|
|
46
|
+
[key]: value.slice(start, i + chunk),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (remaining < additional) {
|
|
50
|
+
if (i > 0 || Object.keys(slice).length > 0) {
|
|
51
|
+
slices.push(Object.assign(slice, i === 0 ? {} : { [key]: value.slice(start, i) }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// prepare for the next slice
|
|
55
|
+
start = i;
|
|
56
|
+
slice = {};
|
|
57
|
+
sliceSize = 2; // getFileSizeInBytes({});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
slice = Object.assign(slice, { [key]: value.slice(start) });
|
|
62
|
+
sliceSize = getFileSizeInBytes(slice);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
slices.push(slice);
|
|
66
|
+
|
|
67
|
+
return slices.map((slice) => Object.assign(slice, ord));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
slice,
|
|
72
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const _ = require("lodash");
|
|
2
|
+
const cds = require("@sap/cds");
|
|
3
|
+
|
|
4
|
+
const Logger = require("../logger");
|
|
5
|
+
const {
|
|
6
|
+
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
7
|
+
DATA_PRODUCT_ANNOTATION,
|
|
8
|
+
DATA_PRODUCT_TYPE,
|
|
9
|
+
DATA_PRODUCT_SIMPLE_ANNOTATION,
|
|
10
|
+
ORD_EXTENSIONS_PREFIX,
|
|
11
|
+
RESOURCE_VISIBILITY,
|
|
12
|
+
ALLOWED_VISIBILITY,
|
|
13
|
+
SUPPORTED_IMPLEMENTATIONSTANDARD_VERSIONS,
|
|
14
|
+
EXTERNAL_SERVICE_ANNOTATION,
|
|
15
|
+
EXTERNAL_DP_ORD_ID_ANNOTATION,
|
|
16
|
+
CDS_ELEMENT_KIND,
|
|
17
|
+
ORD_ACCESS_STRATEGY,
|
|
18
|
+
} = require("../constants");
|
|
19
|
+
|
|
20
|
+
const DEFAULT_ACCESS_STRATEGIES = Object.freeze([ORD_ACCESS_STRATEGY.Open]);
|
|
21
|
+
|
|
22
|
+
function prune(object) {
|
|
23
|
+
return Object.fromEntries(
|
|
24
|
+
Object.entries(object)
|
|
25
|
+
.filter(([, value]) => value !== null && value !== undefined) // remove empty fields
|
|
26
|
+
.filter(([, value]) => typeof value !== "object" || Object.keys(value).length > 0), // remove empty arrays/objects
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getRFC3339Date() {
|
|
31
|
+
const now = new Date();
|
|
32
|
+
const year = now.getUTCFullYear();
|
|
33
|
+
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
34
|
+
const day = String(now.getUTCDate()).padStart(2, "0");
|
|
35
|
+
const hours = String(now.getUTCHours()).padStart(2, "0");
|
|
36
|
+
const minutes = String(now.getUTCMinutes()).padStart(2, "0");
|
|
37
|
+
const seconds = String(now.getUTCSeconds()).padStart(2, "0");
|
|
38
|
+
|
|
39
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}+01:00`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isPrimaryDataProductService(service) {
|
|
43
|
+
return service[DATA_PRODUCT_ANNOTATION] === DATA_PRODUCT_TYPE.primary || !!service[DATA_PRODUCT_SIMPLE_ANNOTATION];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveVisibility(appConfig, service) {
|
|
47
|
+
const explicit = service["@ORD.Extensions.visibility"];
|
|
48
|
+
const standard = service["@ORD.Extensions.implementationStandard"];
|
|
49
|
+
let defaultVisibility = appConfig.env?.defaultVisibility ?? RESOURCE_VISIBILITY.public;
|
|
50
|
+
|
|
51
|
+
//check for supported custom visibility value in defaultVisibility variable
|
|
52
|
+
if (!ALLOWED_VISIBILITY.includes(defaultVisibility)) {
|
|
53
|
+
Logger.warn(
|
|
54
|
+
`Default visibility ${defaultVisibility} is not supported. Using ${RESOURCE_VISIBILITY.public} as fallback`,
|
|
55
|
+
);
|
|
56
|
+
defaultVisibility = RESOURCE_VISIBILITY.public;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Determine visibility
|
|
60
|
+
if (isPrimaryDataProductService(service)) {
|
|
61
|
+
return RESOURCE_VISIBILITY.internal;
|
|
62
|
+
} else if (explicit) {
|
|
63
|
+
return service["@ORD.Extensions.visibility"];
|
|
64
|
+
} else if (SUPPORTED_IMPLEMENTATIONSTANDARD_VERSIONS.includes(standard)) {
|
|
65
|
+
// if the implementationStandard is for example sap:ord-document-api:v1, it should be public by default
|
|
66
|
+
return RESOURCE_VISIBILITY.public;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return defaultVisibility;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveServiceName(appConfig, { name }) {
|
|
73
|
+
return (
|
|
74
|
+
[
|
|
75
|
+
appConfig.internalNamespace, //
|
|
76
|
+
appConfig.ordNamespace, //
|
|
77
|
+
]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.filter((namespace) => name === namespace || name.startsWith(`${namespace}.`))
|
|
80
|
+
.map((namespace) => name.substring(namespace.length))
|
|
81
|
+
.shift()
|
|
82
|
+
?.replace(/^\./, "") ?? name
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function flattenEntityGraph(current, processed = []) {
|
|
87
|
+
return [
|
|
88
|
+
current, //
|
|
89
|
+
...Object.values(current.associations ?? []) //
|
|
90
|
+
.filter(({ target }) => !processed.includes(target))
|
|
91
|
+
.flatMap(({ target, _target }) => {
|
|
92
|
+
processed.push(target);
|
|
93
|
+
return flattenEntityGraph(_target, processed);
|
|
94
|
+
}),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readORDExtensions(model, prefix = ORD_EXTENSIONS_PREFIX) {
|
|
99
|
+
return Object.entries(model) //
|
|
100
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
101
|
+
.map(([key, value]) => [key.substring(prefix.length), value])
|
|
102
|
+
.reduce((result, [key, value]) => _.set(result, key, value), {});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isExternalDataProduct(definition) {
|
|
106
|
+
return !!(
|
|
107
|
+
definition.kind === "service" &&
|
|
108
|
+
definition[EXTERNAL_SERVICE_ANNOTATION] &&
|
|
109
|
+
definition[DATA_PRODUCT_SIMPLE_ANNOTATION] &&
|
|
110
|
+
definition[EXTERNAL_DP_ORD_ID_ANNOTATION]
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isExposedEntityType(definition) {
|
|
115
|
+
return (
|
|
116
|
+
!definition.name.includes(".texts") &&
|
|
117
|
+
definition[ENTITY_RELATIONSHIP_ANNOTATION] &&
|
|
118
|
+
definition.kind === CDS_ELEMENT_KIND.entity &&
|
|
119
|
+
definition["_service"]?.["@protocol"] !== "none" &&
|
|
120
|
+
!isBlockedServiceName(definition["_service"]?.name)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isBlockedServiceName(name) {
|
|
125
|
+
return ["cds.xt.MTXServices", "MtxOrdProviderService", "OpenResourceDiscoveryService"] //
|
|
126
|
+
.some((blocked) => name?.includes(blocked));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isValidService(definition) {
|
|
130
|
+
const isExternalService =
|
|
131
|
+
Object.keys(cds).includes("requires") && Object.keys(cds.requires).includes(definition.name);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
!isExternalService &&
|
|
135
|
+
!definition["@cds.external"] &&
|
|
136
|
+
definition["@protocol"] !== "none" &&
|
|
137
|
+
!isBlockedServiceName(definition.name) &&
|
|
138
|
+
definition.kind === CDS_ELEMENT_KIND.service
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isEventsOnlyService(definition) {
|
|
143
|
+
return (
|
|
144
|
+
Object.keys(definition.events || {}).length > 0 &&
|
|
145
|
+
["actions", "entities", "functions"].every((key) => Object.keys(definition[key] || {}).length === 0)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveAccessStrategies(authConfig, options = {}) {
|
|
150
|
+
const isStrict = options?.strict ?? cds.env.ord?.strictAccessStrategies === true;
|
|
151
|
+
const strategies = _.uniq(authConfig.accessStrategies ?? [])
|
|
152
|
+
.flatMap((strategy) => {
|
|
153
|
+
if (!Object.values(ORD_ACCESS_STRATEGY).includes(strategy)) {
|
|
154
|
+
Logger.warn("resolveAccessStrategies:", `Unknown access strategy type '${strategy}', skipping`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return strategy;
|
|
159
|
+
})
|
|
160
|
+
.filter(Boolean); // Remove null entries
|
|
161
|
+
|
|
162
|
+
if (isStrict && strategies.length === 0) {
|
|
163
|
+
throw new Error("[ORD] accessStrategies missing or empty for resource. Strict mode is enabled");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (strategies.length > 1 && strategies.includes(ORD_ACCESS_STRATEGY.Open)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"Invalid access strategies: 'open' cannot coexist with authenticated strategies (basic-auth, sap:cmp-mtls:v1, sap.businesshub:mtls:v1)",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (strategies.length > 0 ? strategies : DEFAULT_ACCESS_STRATEGIES) //
|
|
173
|
+
.map((strategy) => ({ type: strategy }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
prune,
|
|
178
|
+
isValidService,
|
|
179
|
+
getRFC3339Date,
|
|
180
|
+
readORDExtensions,
|
|
181
|
+
resolveVisibility,
|
|
182
|
+
flattenEntityGraph,
|
|
183
|
+
resolveServiceName,
|
|
184
|
+
isExposedEntityType,
|
|
185
|
+
isEventsOnlyService,
|
|
186
|
+
isBlockedServiceName,
|
|
187
|
+
isExternalDataProduct,
|
|
188
|
+
resolveAccessStrategies,
|
|
189
|
+
isPrimaryDataProductService,
|
|
190
|
+
};
|