@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 +60 -44
- package/lib/utils.js +38 -3
- package/package.json +10 -6
- package/srv/log2als.js +5 -3
- package/srv/log2alsng.js +14 -11
- package/srv/log2restv2.js +157 -111
- package/srv/service.js +2 -1
package/cds-plugin.js
CHANGED
|
@@ -1,69 +1,85 @@
|
|
|
1
|
-
const cds = require(
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
2
|
|
|
3
|
-
const { auditAccess } = require(
|
|
4
|
-
const {
|
|
5
|
-
|
|
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 = [
|
|
12
|
+
const WRITE = ["CREATE", "UPDATE", "DELETE"];
|
|
8
13
|
|
|
9
14
|
/*
|
|
10
15
|
* Add generic audit logging handlers
|
|
11
16
|
*/
|
|
12
|
-
cds.on(
|
|
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)
|
|
20
|
-
|
|
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[
|
|
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 (
|
|
28
|
-
e[
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
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 =>
|
|
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.
|
|
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
|
+
"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
|
|
2
|
-
const isV3 = credentials['user-provided']?.some(obj => obj.tags.includes('auditlog-ng'))
|
|
1
|
+
const cds = require("@sap/cds");
|
|
3
2
|
|
|
4
|
-
module.exports =
|
|
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
|
-
|
|
12
|
-
|
|
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: `/${
|
|
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:
|
|
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
|
|
166
|
-
|
|
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:
|
|
176
|
-
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(
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
|
+
const { appMetadata } = require("../lib/utils");
|
|
2
3
|
|
|
3
|
-
const LOG = cds.log(
|
|
4
|
+
const LOG = cds.log("audit-log");
|
|
4
5
|
|
|
5
|
-
const AuditLogService = require(
|
|
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)
|
|
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 =
|
|
14
|
-
this._auth =
|
|
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/) ?
|
|
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(
|
|
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 ===
|
|
27
|
-
return this._handle(data,
|
|
31
|
+
if (event === "SensitiveDataRead" || event.match(/^dataAccess/i)) {
|
|
32
|
+
return this._handle(data, "DATA_ACCESS");
|
|
28
33
|
}
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
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 ===
|
|
34
|
-
data.success = true
|
|
35
|
-
return this._handle(data,
|
|
41
|
+
if (event === "ConfigurationModified" || event.match(/^configChange/i)) {
|
|
42
|
+
data.success = true;
|
|
43
|
+
return this._handle(data, "CONFIGURATION_CHANGE");
|
|
36
44
|
}
|
|
37
|
-
if (event ===
|
|
38
|
-
if (typeof data.data ===
|
|
39
|
-
|
|
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) +
|
|
55
|
-
const data = {
|
|
56
|
-
|
|
57
|
-
|
|
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[
|
|
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 ?
|
|
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(
|
|
70
|
-
|
|
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(
|
|
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$/))
|
|
78
|
-
|
|
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 = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
headers.
|
|
87
|
-
headers.
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 ===
|
|
97
|
-
headers.authorization =
|
|
98
|
-
data.tenant =
|
|
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, {
|
|
102
|
-
|
|
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(
|
|
136
|
+
LOG._trace && LOG.trace("error during log send:", err);
|
|
108
137
|
// 429 (rate limit) is not unrecoverable
|
|
109
|
-
if (
|
|
110
|
-
err.
|
|
111
|
-
|
|
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(
|
|
121
|
-
|
|
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:
|
|
132
|
-
DATA_MODIFICATION:
|
|
133
|
-
CONFIGURATION_CHANGE:
|
|
134
|
-
SECURITY_EVENT:
|
|
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:
|
|
138
|
-
DATA_MODIFICATION:
|
|
139
|
-
CONFIGURATION_CHANGE:
|
|
140
|
-
SECURITY_EVENT:
|
|
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(
|
|
182
|
+
const https = require("https");
|
|
149
183
|
|
|
150
184
|
async function _post(url, data, options) {
|
|
151
|
-
options.method ??=
|
|
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(
|
|
156
|
-
res.on(
|
|
157
|
-
const { statusCode
|
|
158
|
-
let body = Buffer.concat(chunks).toString()
|
|
159
|
-
if (res.headers[
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 =
|
|
166
|
-
|
|
167
|
-
|
|
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(
|
|
174
|
-
req.write(typeof 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(
|
|
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
|
|
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
|