@cap-js/audit-logging 1.1.0 → 1.2.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/cds-plugin.js CHANGED
@@ -1,69 +1,85 @@
1
- const cds = require('@sap/cds')
1
+ const cds = require("@sap/cds");
2
2
 
3
- const { auditAccess } = require('./lib/access')
4
- const { addDiffToCtx, calcModLogs4Before, calcModLogs4After, emitModLogs } = require('./lib/modification')
5
- const { hasPersonalData } = require('./lib/utils')
3
+ const { auditAccess } = require("./lib/access");
4
+ const {
5
+ addDiffToCtx,
6
+ calcModLogs4Before,
7
+ calcModLogs4After,
8
+ emitModLogs,
9
+ } = require("./lib/modification");
10
+ const { hasPersonalData } = require("./lib/utils");
6
11
 
7
- const WRITE = ['CREATE', 'UPDATE', 'DELETE']
12
+ const WRITE = ["CREATE", "UPDATE", "DELETE"];
8
13
 
9
14
  /*
10
15
  * Add generic audit logging handlers
11
16
  */
12
- cds.on('served', services => {
13
- const db = cds.db
17
+ cds.on("served", (services) => {
18
+ const db = cds.db;
14
19
 
15
20
  for (const service of services) {
16
- if (!(service instanceof cds.ApplicationService)) continue
21
+ if (!(service instanceof cds.ApplicationService)) continue;
17
22
 
18
- const relevantEntities = []
19
- for (const entity of service.entities) if (hasPersonalData(entity)) relevantEntities.push(entity)
20
- if (!relevantEntities.length) continue
23
+ const relevantEntities = [];
24
+ for (const entity of service.entities)
25
+ if (hasPersonalData(entity)) relevantEntities.push(entity);
26
+ if (!relevantEntities.length) continue;
21
27
 
22
28
  // automatically promote entities that are associated with data subjects
23
29
  for (const entity of relevantEntities) {
24
- if (entity['@PersonalData.EntitySemantics'] !== 'DataSubject') continue
30
+ if (entity["@PersonalData.EntitySemantics"] !== "DataSubject") continue;
25
31
  for (const e of service.entities) {
26
32
  for (const k in e.associations) {
27
- if (e.associations[k].target === entity.name && k !== 'SiblingEntity') {
28
- e['@PersonalData.EntitySemantics'] ??= 'Other'
29
- e.associations[k]['@PersonalData.FieldSemantics'] ??= 'DataSubjectID'
30
- if (!relevantEntities.includes(e)) relevantEntities.push(e)
33
+ if (
34
+ e.associations[k].target === entity.name &&
35
+ k !== "SiblingEntity"
36
+ ) {
37
+ e["@PersonalData.EntitySemantics"] ??= "Other";
38
+ e.associations[k]["@PersonalData.FieldSemantics"] ??=
39
+ "DataSubjectID";
40
+ if (!relevantEntities.includes(e)) relevantEntities.push(e);
31
41
  }
32
42
  }
33
43
  }
34
44
  }
45
+ }
46
+ for (const service of services) {
47
+ if (!(service instanceof cds.ApplicationService)) continue;
48
+ service.after("READ", async (res, req) => {
49
+ // Checking for req.target._service to make sure only entities within services are considered
50
+ if (!req.target._service || !hasPersonalData(req.target)) return;
51
+ await auditAccess.call(service, res, req);
52
+ });
35
53
 
36
- for (const entity of relevantEntities) {
37
- /*
38
- * data access
39
- */
40
- service.after('READ', entity, auditAccess)
41
-
42
- /*
43
- * data modification
44
- */
45
- // common
46
- db.before(WRITE, entity, addDiffToCtx)
47
- service.after(WRITE, entity, emitModLogs)
48
- /*
49
- * for new or modified data, modifications are calculated in after phase
50
- * for deleted data, modifications are calculated in before phase
51
- * deep updates can contain new, modified and deleted data -> both phases
52
- */
53
- // create
54
- db.after('CREATE', entity, calcModLogs4After)
55
- // update
56
- db.before('UPDATE', entity, calcModLogs4Before)
57
- db.after('UPDATE', entity, calcModLogs4After)
58
- // delete
59
- db.before('DELETE', entity, calcModLogs4Before)
60
- }
54
+ service.after(WRITE, async (res, req) => {
55
+ if (!req.target._service || !hasPersonalData(req.target)) return;
56
+ await emitModLogs.call(service, res, req);
57
+ });
61
58
  }
62
- })
59
+
60
+ db.before("CREATE", async (req) => {
61
+ if (!req.target._service || !hasPersonalData(req.target)) return;
62
+ await addDiffToCtx.call(db, req);
63
+ });
64
+ /*
65
+ * for new or modified data, modifications are calculated in after phase
66
+ * for deleted data, modifications are calculated in before phase
67
+ * deep updates can contain new, modified and deleted data -> both phases
68
+ */
69
+ db.after(["CREATE", "UPDATE"], async (res, req) => {
70
+ if (!req.target._service || !hasPersonalData(req.target)) return;
71
+ await calcModLogs4After.call(db, res, req);
72
+ });
73
+ db.before(["DELETE", "UPDATE"], async (req) => {
74
+ if (!req.target._service || !hasPersonalData(req.target)) return;
75
+ await addDiffToCtx.call(db, req);
76
+ await calcModLogs4Before.call(db, req);
77
+ });
78
+ });
63
79
 
64
80
  /*
65
81
  * Export base class for extending in custom implementations
66
82
  */
67
83
  module.exports = {
68
- AuditLogService: require('./srv/service')
69
- }
84
+ AuditLogService: require("./srv/service"),
85
+ };
package/lib/utils.js CHANGED
@@ -108,7 +108,11 @@ const _keyColumns = (keys, alias) => {
108
108
  .map(key => ({ ref: [alias, key.name] }))
109
109
  }
110
110
 
111
- const _alias = entity => entity.name.replace(`${entity._service.name}.`, '').replace('.', '_')
111
+ const _alias = entity => {
112
+ // REVISIT: we should not rely on entity._service (but I don't want to break existing behavior right now)
113
+ if (!entity._service) return `${entity}`
114
+ return entity.name.replace(`${entity._service.name}.`, '').replace(/\./g, '_')
115
+ }
112
116
 
113
117
  const _buildSubSelect = (model, { entity, relative, element, next }, row, previousCqn) => {
114
118
  // relative is a parent or an entity itself
@@ -282,6 +286,36 @@ const resolveDataSubjects = (logs, req) => {
282
286
  })
283
287
  }
284
288
 
289
+ const getAppMetadata = () => {
290
+ const appMetadata = cds.env.app;
291
+
292
+ if (appMetadata) {
293
+ return {
294
+ appID: appMetadata.id,
295
+ appName: appMetadata.name,
296
+ appURL: appMetadata.url,
297
+ organization_name: appMetadata.organization_name,
298
+ space_name: appMetadata.space_name,
299
+ };
300
+ }
301
+
302
+ // fallback: if the app metadata is undefined, then extract the metadata from the underlying environment (CF/Kyma/...)
303
+ const vcapApplication =
304
+ process.env.VCAP_APPLICATION && JSON.parse(process.env.VCAP_APPLICATION);
305
+
306
+ return {
307
+ appID: vcapApplication && vcapApplication.application_id,
308
+ appName: vcapApplication && vcapApplication.application_name,
309
+ organization_name: vcapApplication && vcapApplication.organization_name,
310
+ space_name: vcapApplication && vcapApplication.space_name,
311
+ appURL:
312
+ vcapApplication &&
313
+ vcapApplication.application_uris &&
314
+ vcapApplication.application_uris[0] &&
315
+ `https://${vcapApplication.application_uris[0].replace(/^https?:\/\//, "")}`,
316
+ };
317
+ };
318
+
285
319
  module.exports = {
286
320
  hasPersonalData,
287
321
  getMapKeyForCurrentRequest,
@@ -291,5 +325,6 @@ module.exports = {
291
325
  addObjectID,
292
326
  addDataSubject,
293
327
  addDataSubjectForDetailsEntity,
294
- resolveDataSubjects
295
- }
328
+ resolveDataSubjects,
329
+ appMetadata: getAppMetadata(),
330
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/audit-logging",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CDS plugin providing integration to the SAP Audit Log service as well as out-of-the-box personal data-related audit logging based on annotations.",
5
5
  "repository": "cap-js/audit-logging",
6
6
  "author": "SAP SE (https://www.sap.com)",
@@ -22,10 +22,11 @@
22
22
  "@cap-js/audit-logging": "file:.",
23
23
  "@cap-js/cds-test": ">=0",
24
24
  "@cap-js/sqlite": ">=1",
25
+ "@sap/cds-mtxs": "^3.7.1",
25
26
  "axios": "^1",
26
27
  "eslint": "^9",
27
28
  "express": "^4",
28
- "jest": "^29",
29
+ "jest": "^30",
29
30
  "pretty-quick": "^4.2.2",
30
31
  "simple-git-hooks": "^2.13.1"
31
32
  },
@@ -48,12 +49,12 @@
48
49
  }
49
50
  },
50
51
  "kinds": {
51
- "audit-log-to-console": {
52
- "impl": "@cap-js/audit-logging/srv/log2console"
53
- },
54
52
  "audit-log-to-als": {
55
53
  "impl": "@cap-js/audit-logging/srv/log2als"
56
54
  },
55
+ "audit-log-to-console": {
56
+ "impl": "@cap-js/audit-logging/srv/log2console"
57
+ },
57
58
  "audit-log-to-restv2": {
58
59
  "impl": "@cap-js/audit-logging/srv/log2restv2",
59
60
  "vcap": {
@@ -61,7 +62,10 @@
61
62
  }
62
63
  },
63
64
  "audit-log-to-alsng": {
64
- "impl": "@cap-js/audit-logging/srv/log2alsng"
65
+ "impl": "@cap-js/audit-logging/srv/log2alsng",
66
+ "vcap": {
67
+ "name": "auditlog-ng"
68
+ }
65
69
  }
66
70
  }
67
71
  }
package/srv/log2als.js CHANGED
@@ -1,4 +1,6 @@
1
- const credentials = JSON.parse(process.env.VCAP_SERVICES) || {}
2
- const isV3 = credentials['user-provided']?.some(obj => obj.tags.includes('auditlog-ng'))
1
+ const cds = require("@sap/cds");
3
2
 
4
- module.exports = isV3 ? require('./log2alsng') : require('./log2restv2')
3
+ module.exports =
4
+ cds.env.requires["audit-log"].vcap.name === "auditlog-ng"
5
+ ? require("./log2alsng")
6
+ : require("./log2restv2");
package/srv/log2alsng.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const cds = require('@sap/cds')
2
2
  const LOG = cds.log('audit-log')
3
-
3
+ const { appMetadata } = require("../lib/utils");
4
4
  const https = require('https')
5
5
 
6
6
  const AuditLogService = require('./service')
@@ -8,10 +8,8 @@ const AuditLogService = require('./service')
8
8
  module.exports = class AuditLog2ALSNG extends AuditLogService {
9
9
  constructor() {
10
10
  super()
11
- this._vcap = JSON.parse(process.env.VCAP_SERVICES || '{}')
12
- this._userProvided = this._vcap['user-provided']?.find(obj => obj.tags.includes('auditlog-ng')) || {}
13
- if (!this._userProvided.credentials) throw new Error('No credentials found for SAP Audit Log Service NG')
14
- this._vcapApplication = JSON.parse(process.env.VCAP_APPLICATION || '{}')
11
+ if (!cds.env.requires['audit-log']?.credentials)
12
+ throw new Error('No credentials found for SAP Audit Log Service NG')
15
13
  }
16
14
 
17
15
  async init() {
@@ -115,13 +113,13 @@ module.exports = class AuditLog2ALSNG extends AuditLogService {
115
113
  const eventData = {
116
114
  id: cds.utils.uuid(),
117
115
  specversion: 1,
118
- source: `/${this._userProvided.credentials?.region}/${this._userProvided.credentials?.namespace}/${tenant}`,
116
+ source: `/${cds.env.requires["audit-log"].credentials?.region}/${cds.env.requires["audit-log"].credentials?.namespace}/${tenant}`,
119
117
  type: event,
120
118
  time: timestamp,
121
119
  data: {
122
120
  metadata: {
123
121
  ts: timestamp,
124
- appId: this._vcapApplication.application_id || 'default app',
122
+ appId: appMetadata.appID || "default app",
125
123
  infrastructure: {
126
124
  other: {
127
125
  runtimeType: 'Node.js'
@@ -162,8 +160,13 @@ module.exports = class AuditLog2ALSNG extends AuditLogService {
162
160
  }
163
161
 
164
162
  logEvent(event, data) {
165
- const passphrase = this._userProvided.credentials?.keyPassphrase
166
- const url = new URL(`${this._userProvided.credentials?.url}/ingestion/v1/events`)
163
+ const credentials = cds.env.requires["audit-log"].credentials;
164
+ if (!credentials) {
165
+ throw new Error("No credentials found for SAP Audit Log Service NG");
166
+ }
167
+
168
+ const passphrase = credentials.keyPassphrase
169
+ const url = new URL(`${credentials.url}/ingestion/v1/events`)
167
170
  const eventData = this.formatEventData(event, data)
168
171
 
169
172
  const options = {
@@ -172,8 +175,8 @@ module.exports = class AuditLog2ALSNG extends AuditLogService {
172
175
  'Content-Type': 'application/json',
173
176
  'Content-Length': Buffer.byteLength(eventData)
174
177
  },
175
- key: this._userProvided.credentials?.key,
176
- cert: this._userProvided.credentials?.cert,
178
+ key: credentials.key,
179
+ cert: credentials.cert,
177
180
  ...(passphrase !== undefined && { passphrase })
178
181
  }
179
182
 
package/srv/log2restv2.js CHANGED
@@ -1,126 +1,160 @@
1
- const cds = require('@sap/cds')
1
+ const cds = require("@sap/cds");
2
+ const { appMetadata } = require("../lib/utils");
2
3
 
3
- const LOG = cds.log('audit-log')
4
+ const LOG = cds.log("audit-log");
4
5
 
5
- const AuditLogService = require('./service')
6
+ const AuditLogService = require("./service");
6
7
 
7
8
  module.exports = class AuditLog2RESTv2 extends AuditLogService {
8
9
  async init() {
9
10
  // credentials stuff
10
- const { credentials } = this.options
11
- if (!credentials) throw new Error('No or malformed credentials for "audit-log"')
11
+ const { credentials } = this.options;
12
+ if (!credentials)
13
+ throw new Error('No or malformed credentials for "audit-log"');
12
14
  if (!credentials.uaa) {
13
- this._plan = 'standard'
14
- this._auth = 'Basic ' + Buffer.from(credentials.user + ':' + credentials.password).toString('base64')
15
+ this._plan = "standard";
16
+ this._auth =
17
+ "Basic " +
18
+ Buffer.from(credentials.user + ":" + credentials.password).toString(
19
+ "base64",
20
+ );
15
21
  } else {
16
- this._plan = credentials.url.match(/6081/) ? 'premium' : 'oauth2'
17
- this._tokens = new Map()
18
- this._provider = credentials.uaa.tenantid
22
+ this._plan = credentials.url.match(/6081/) ? "premium" : "oauth2";
23
+ this._tokens = new Map();
24
+ this._provider = credentials.uaa.tenantid;
19
25
  }
20
- this._vcap = process.env.VCAP_APPLICATION ? JSON.parse(process.env.VCAP_APPLICATION) : null
21
26
 
22
- this.on('*', function (req) {
23
- const { event, data } = req
27
+ this.on("*", function (req) {
28
+ const { event, data } = req;
24
29
 
25
30
  // event.match() is used to support the old event names
26
- if (event === 'SensitiveDataRead' || event.match(/^dataAccess/i)) {
27
- return this._handle(data, 'DATA_ACCESS')
31
+ if (event === "SensitiveDataRead" || event.match(/^dataAccess/i)) {
32
+ return this._handle(data, "DATA_ACCESS");
28
33
  }
29
- if (event === 'PersonalDataModified' || event.match(/^dataModification/i)) {
30
- data.success = true
31
- return this._handle(data, 'DATA_MODIFICATION')
34
+ if (
35
+ event === "PersonalDataModified" ||
36
+ event.match(/^dataModification/i)
37
+ ) {
38
+ data.success = true;
39
+ return this._handle(data, "DATA_MODIFICATION");
32
40
  }
33
- if (event === 'ConfigurationModified' || event.match(/^configChange/i)) {
34
- data.success = true
35
- return this._handle(data, 'CONFIGURATION_CHANGE')
41
+ if (event === "ConfigurationModified" || event.match(/^configChange/i)) {
42
+ data.success = true;
43
+ return this._handle(data, "CONFIGURATION_CHANGE");
36
44
  }
37
- if (event === 'SecurityEvent' || event.match(/^security/i)) {
38
- if (typeof data.data === 'object') data.data = JSON.stringify(data.data)
39
- return this._handle(data, 'SECURITY_EVENT')
45
+ if (event === "SecurityEvent" || event.match(/^security/i)) {
46
+ if (typeof data.data === "object")
47
+ data.data = JSON.stringify(data.data);
48
+ return this._handle(data, "SECURITY_EVENT");
40
49
  }
41
50
 
42
- LOG._warn && LOG.warn(`Event "${event}" is not implemented`)
43
- })
51
+ LOG._warn && LOG.warn(`Event "${event}" is not implemented`);
52
+ });
44
53
 
45
54
  // call AuditLogService's init
46
- await super.init()
55
+ await super.init();
47
56
  }
48
57
 
49
58
  async _getToken(tenant) {
50
- const { _tokens: tokens } = this
51
- if (tokens.has(tenant)) return tokens.get(tenant)
52
-
53
- const { uaa } = this.options.credentials
54
- const url = (uaa.certurl || uaa.url) + '/oauth/token'
55
- const data = { grant_type: 'client_credentials', response_type: 'token', client_id: uaa.clientid }
56
- const options = { headers: { 'content-type': 'application/x-www-form-urlencoded' } }
57
- if (tenant !== this._provider) options.headers['x-zid'] = tenant
59
+ const { _tokens: tokens } = this;
60
+ if (tokens.has(tenant)) return tokens.get(tenant);
61
+
62
+ const { uaa } = this.options.credentials;
63
+ const url = (uaa.certurl || uaa.url) + "/oauth/token";
64
+ const data = {
65
+ grant_type: "client_credentials",
66
+ response_type: "token",
67
+ client_id: uaa.clientid,
68
+ };
69
+ const options = {
70
+ headers: { "content-type": "application/x-www-form-urlencoded" },
71
+ };
72
+ if (tenant !== this._provider) options.headers["x-zid"] = tenant;
73
+
58
74
  // certificate or secret?
59
- if (uaa['credential-type'] === 'x509') {
60
- options.agent = new https.Agent({ cert: uaa.certificate, key: uaa.key })
75
+ if (uaa["credential-type"] === "x509") {
76
+ options.agent = new https.Agent({ cert: uaa.certificate, key: uaa.key });
61
77
  } else {
62
- data.client_secret = uaa.clientsecret
78
+ data.client_secret = uaa.clientsecret;
63
79
  }
64
80
  const urlencoded = Object.keys(data).reduce((acc, cur) => {
65
- acc += (acc ? '&' : '') + cur + '=' + data[cur]
66
- return acc
67
- }, '')
81
+ acc += (acc ? "&" : "") + cur + "=" + data[cur];
82
+ return acc;
83
+ }, "");
68
84
  try {
69
- const { access_token, expires_in } = await _post(url, urlencoded, options)
70
- tokens.set(tenant, access_token)
85
+ const { access_token, expires_in } = await _post(
86
+ url,
87
+ urlencoded,
88
+ options,
89
+ );
90
+ tokens.set(tenant, access_token);
71
91
  // remove token from cache 60 seconds before it expires
72
- setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000)
73
- return access_token
92
+ setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000);
93
+ return access_token;
74
94
  } catch (err) {
75
- LOG._trace && LOG.trace('error during token fetch:', err)
95
+ LOG._trace && LOG.trace("error during token fetch:", err);
76
96
  // 401 could also mean x-zid is not valid
77
- if (String(err.response?.statusCode).match(/^4\d\d$/)) err.unrecoverable = true
78
- throw err
97
+ if (String(err.response?.statusCode).match(/^4\d\d$/))
98
+ err.unrecoverable = true;
99
+ throw err;
79
100
  }
80
101
  }
81
102
 
82
103
  async _send(data, path) {
83
- const headers = { 'content-type': 'application/json;charset=utf-8' }
84
- if (this._vcap) {
85
- headers.XS_AUDIT_ORG = this._vcap.organization_name
86
- headers.XS_AUDIT_SPACE = this._vcap.space_name
87
- headers.XS_AUDIT_APP = this._vcap.application_name
104
+ const headers = { "content-type": "application/json;charset=utf-8" };
105
+
106
+ if (appMetadata.appName) {
107
+ headers.XS_AUDIT_ORG = appMetadata.organization_name;
108
+ headers.XS_AUDIT_SPACE = appMetadata.space_name;
109
+ headers.XS_AUDIT_APP = appMetadata.appName;
88
110
  }
89
- let url
90
- if (this._plan === 'standard') {
91
- url = this.options.credentials.url + PATHS.STANDARD[path]
92
- headers.authorization = this._auth
111
+
112
+ let url;
113
+ if (this._plan === "standard") {
114
+ url = this.options.credentials.url + PATHS.STANDARD[path];
115
+ headers.authorization = this._auth;
93
116
  } else {
94
- url = this.options.credentials.url + PATHS.OAUTH2[path]
95
- data.tenant ??= this._provider //> if request has no tenant, stay in provider account
96
- if (data.tenant === '$PROVIDER') data.tenant = this._provider
97
- headers.authorization = 'Bearer ' + (await this._getToken(data.tenant))
98
- data.tenant = data.tenant === this._provider ? '$PROVIDER' : '$SUBSCRIBER'
117
+ url = this.options.credentials.url + PATHS.OAUTH2[path];
118
+ data.tenant ??= this._provider; //> if request has no tenant, stay in provider account
119
+ if (data.tenant === "$PROVIDER") data.tenant = this._provider;
120
+ headers.authorization = "Bearer " + (await this._getToken(data.tenant));
121
+ data.tenant =
122
+ data.tenant === this._provider ? "$PROVIDER" : "$SUBSCRIBER";
99
123
  }
100
124
  if (LOG._debug) {
101
- const _headers = Object.assign({}, headers, { authorization: headers.authorization.split(' ')[0] + ' ***' })
102
- LOG.debug(`sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`, _headers)
125
+ const _headers = Object.assign({}, headers, {
126
+ authorization: headers.authorization.split(" ")[0] + " ***",
127
+ });
128
+ LOG.debug(
129
+ `sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`,
130
+ _headers,
131
+ );
103
132
  }
104
133
  try {
105
- await _post(url, data, { headers })
134
+ await _post(url, data, { headers });
106
135
  } catch (err) {
107
- LOG._trace && LOG.trace('error during log send:', err)
136
+ LOG._trace && LOG.trace("error during log send:", err);
108
137
  // 429 (rate limit) is not unrecoverable
109
- if (String(err.response?.statusCode).match(/^4\d\d$/) && err.response?.statusCode !== 429)
110
- err.unrecoverable = true
111
- throw err
138
+ if (
139
+ String(err.response?.statusCode).match(/^4\d\d$/) &&
140
+ err.response?.statusCode !== 429
141
+ )
142
+ err.unrecoverable = true;
143
+ throw err;
112
144
  }
113
145
  }
114
146
 
115
147
  async _handle(logs, path) {
116
- if (!Array.isArray(logs)) logs = [logs]
148
+ if (!Array.isArray(logs)) logs = [logs];
117
149
 
118
150
  // write the logs
119
- const errors = []
120
- await Promise.all(logs.map(log => this._send(log, path).catch(err => errors.push(err))))
121
- if (errors.length) throw _getErrorToThrow(errors)
151
+ const errors = [];
152
+ await Promise.all(
153
+ logs.map((log) => this._send(log, path).catch((err) => errors.push(err))),
154
+ );
155
+ if (errors.length) throw _getErrorToThrow(errors);
122
156
  }
123
- }
157
+ };
124
158
 
125
159
  /*
126
160
  * consts
@@ -128,58 +162,70 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
128
162
 
129
163
  const PATHS = {
130
164
  STANDARD: {
131
- DATA_ACCESS: '/audit-log/v2/data-accesses',
132
- DATA_MODIFICATION: '/audit-log/v2/data-modifications',
133
- CONFIGURATION_CHANGE: '/audit-log/v2/configuration-changes',
134
- SECURITY_EVENT: '/audit-log/v2/security-events'
165
+ DATA_ACCESS: "/audit-log/v2/data-accesses",
166
+ DATA_MODIFICATION: "/audit-log/v2/data-modifications",
167
+ CONFIGURATION_CHANGE: "/audit-log/v2/configuration-changes",
168
+ SECURITY_EVENT: "/audit-log/v2/security-events",
135
169
  },
136
170
  OAUTH2: {
137
- DATA_ACCESS: '/audit-log/oauth2/v2/data-accesses',
138
- DATA_MODIFICATION: '/audit-log/oauth2/v2/data-modifications',
139
- CONFIGURATION_CHANGE: '/audit-log/oauth2/v2/configuration-changes',
140
- SECURITY_EVENT: '/audit-log/oauth2/v2/security-events'
141
- }
142
- }
171
+ DATA_ACCESS: "/audit-log/oauth2/v2/data-accesses",
172
+ DATA_MODIFICATION: "/audit-log/oauth2/v2/data-modifications",
173
+ CONFIGURATION_CHANGE: "/audit-log/oauth2/v2/configuration-changes",
174
+ SECURITY_EVENT: "/audit-log/oauth2/v2/security-events",
175
+ },
176
+ };
143
177
 
144
178
  /*
145
179
  * utils
146
180
  */
147
181
 
148
- const https = require('https')
182
+ const https = require("https");
149
183
 
150
184
  async function _post(url, data, options) {
151
- options.method ??= 'POST'
185
+ options.method ??= "POST";
152
186
  return new Promise((resolve, reject) => {
153
- const req = https.request(url, options, res => {
154
- const chunks = []
155
- res.on('data', chunk => chunks.push(chunk))
156
- res.on('end', () => {
157
- const { statusCode, statusMessage } = res
158
- let body = Buffer.concat(chunks).toString()
159
- if (res.headers['content-type']?.match(/json/)) body = JSON.parse(body)
187
+ const req = https.request(url, options, (res) => {
188
+ const chunks = [];
189
+ res.on("data", (chunk) => chunks.push(chunk));
190
+ res.on("end", () => {
191
+ const { statusCode } = res;
192
+ let body = Buffer.concat(chunks).toString();
193
+ if (res.headers["content-type"]?.match(/json/)) body = JSON.parse(body);
160
194
  if (res.statusCode >= 400) {
161
- // prettier-ignore
162
- const err = new Error(`Request failed with${statusMessage ? `: ${statusCode} - ${statusMessage}` : ` status ${statusCode}`}`)
163
- err.request = { method: options.method, url, headers: options.headers, body: data }
195
+ const message =
196
+ body && typeof body === "object" && !Array.isArray(body)
197
+ ? Object.values(body).join(" - ")
198
+ : "";
199
+ LOG._trace && LOG.trace(`Request body of failed audit-log: `, data);
200
+ const err = new Error(
201
+ `Request failed with${message ? `: ${statusCode} - ${message}` : ` status ${statusCode}`}`,
202
+ );
203
+ err.request = {
204
+ method: options.method,
205
+ url,
206
+ headers: options.headers,
207
+ body: data,
208
+ };
164
209
  if (err.request.headers.authorization)
165
- err.request.headers.authorization = err.request.headers.authorization.split(' ')[0] + ' ***'
166
- err.response = { statusCode, statusMessage, headers: res.headers, body }
167
- reject(err)
210
+ err.request.headers.authorization =
211
+ err.request.headers.authorization.split(" ")[0] + " ***";
212
+ err.response = { statusCode, headers: res.headers, body };
213
+ reject(err);
168
214
  } else {
169
- resolve(body)
215
+ resolve(body);
170
216
  }
171
- })
172
- })
173
- req.on('error', reject)
174
- req.write(typeof data === 'object' ? JSON.stringify(data) : data)
175
- req.end()
176
- })
217
+ });
218
+ });
219
+ req.on("error", reject);
220
+ req.write(typeof data === "object" ? JSON.stringify(data) : data);
221
+ req.end();
222
+ });
177
223
  }
178
224
 
179
225
  function _getErrorToThrow(errors) {
180
- if (errors.length === 1) return errors[0]
181
- const error = new cds.error('MULTIPLE_ERRORS')
182
- error.details = errors
183
- if (errors.some(e => e.unrecoverable)) error.unrecoverable = true
184
- return error
226
+ if (errors.length === 1) return errors[0];
227
+ const error = new cds.error("MULTIPLE_ERRORS");
228
+ error.details = errors;
229
+ if (errors.some((e) => e.unrecoverable)) error.unrecoverable = true;
230
+ return error;
185
231
  }
package/srv/service.js CHANGED
@@ -6,7 +6,8 @@ module.exports = class AuditLogService extends cds.Service {
6
6
  this.before('*', req => {
7
7
  const { tenant, user, timestamp } = cds.context
8
8
  req.data.uuid ??= cds.utils.uuid()
9
- // allows to specify undefined tenant in order to log to provider in multi-tenant scenarios
9
+ // allows to specify null as tenant in order to log to provider in multi-tenant scenarios
10
+ // NOTE: tenant: null is not a public API!
10
11
  if (!('tenant' in req.data)) req.data.tenant = tenant
11
12
  req.data.user ??= user.id
12
13
  req.data.time ??= timestamp