@cap-js/audit-logging 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -10
- package/cds-plugin.js +56 -44
- package/lib/_relation.js +103 -100
- package/lib/access.js +38 -37
- package/lib/modification.js +117 -108
- package/lib/utils.js +200 -157
- package/package.json +14 -11
- package/srv/log2als.js +5 -3
- package/srv/log2alsng.js +111 -105
- package/srv/log2console.js +7 -7
- package/srv/log2restv2.js +136 -109
- package/srv/service.js +17 -16
package/lib/utils.js
CHANGED
|
@@ -1,278 +1,291 @@
|
|
|
1
|
-
const cds = require(
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
2
|
|
|
3
|
-
const { Relation, exposeRelation, relationHandler } = require(
|
|
3
|
+
const { Relation, exposeRelation, relationHandler } = require("./_relation");
|
|
4
4
|
|
|
5
|
-
const WRITE = { CREATE: 1, UPDATE: 1, DELETE: 1 }
|
|
5
|
+
const WRITE = { CREATE: 1, UPDATE: 1, DELETE: 1 };
|
|
6
6
|
|
|
7
|
-
const $hasPersonalData = Symbol(
|
|
8
|
-
const $dataSubject = Symbol(
|
|
9
|
-
const $parents = Symbol(
|
|
10
|
-
const $visitedUp = Symbol(
|
|
11
|
-
const $visitedDown = Symbol(
|
|
7
|
+
const $hasPersonalData = Symbol("@cap-js/audit-logging:hasPersonalData");
|
|
8
|
+
const $dataSubject = Symbol("@cap-js/audit-logging:dataSubject");
|
|
9
|
+
const $parents = Symbol("@cap-js/audit-logging:parents");
|
|
10
|
+
const $visitedUp = Symbol("@cap-js/audit-logging:visitedUp");
|
|
11
|
+
const $visitedDown = Symbol("@cap-js/audit-logging:visitedDown");
|
|
12
12
|
|
|
13
|
-
const hasPersonalData = entity => {
|
|
13
|
+
const hasPersonalData = (entity) => {
|
|
14
14
|
if (entity.own($hasPersonalData) == null) {
|
|
15
|
-
if (!entity[
|
|
15
|
+
if (!entity["@PersonalData.EntitySemantics"]) entity.set($hasPersonalData, false);
|
|
16
16
|
else {
|
|
17
17
|
// default role to entity name
|
|
18
|
-
if (
|
|
19
|
-
entity[
|
|
18
|
+
if (
|
|
19
|
+
entity["@PersonalData.EntitySemantics"] === "DataSubject" &&
|
|
20
|
+
!entity["@PersonalData.DataSubjectRole"]
|
|
21
|
+
)
|
|
22
|
+
entity["@PersonalData.DataSubjectRole"] = entity.name.match(/\w+/g).pop();
|
|
20
23
|
// prettier-ignore
|
|
21
24
|
const hasPersonalData = !!Object.values(entity.elements).some(element =>
|
|
22
25
|
element['@PersonalData.IsPotentiallyPersonal'] ||
|
|
23
26
|
element['@PersonalData.IsPotentiallySensitive'] ||
|
|
24
27
|
(element['@PersonalData.FieldSemantics'] && element['@PersonalData.FieldSemantics'] === 'DataSubjectID'))
|
|
25
|
-
entity.set($hasPersonalData, hasPersonalData)
|
|
28
|
+
entity.set($hasPersonalData, hasPersonalData);
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
|
-
return entity.own($hasPersonalData)
|
|
29
|
-
}
|
|
31
|
+
return entity.own($hasPersonalData);
|
|
32
|
+
};
|
|
30
33
|
|
|
31
|
-
const getMapKeyForCurrentRequest = req => {
|
|
34
|
+
const getMapKeyForCurrentRequest = (req) => {
|
|
32
35
|
// running in srv or db layer? -> srv's req.query used as key of diff and logs maps at req.context
|
|
33
36
|
// REVISIT: req._tx should not be used like that!
|
|
34
|
-
return req.tx.isDatabaseService ? req._.query : req.query
|
|
35
|
-
}
|
|
37
|
+
return req.tx.isDatabaseService ? req._.query : req.query;
|
|
38
|
+
};
|
|
36
39
|
|
|
37
|
-
const getRootEntity = element => {
|
|
38
|
-
let entity = element.parent
|
|
39
|
-
while (entity.kind !==
|
|
40
|
-
return entity
|
|
41
|
-
}
|
|
40
|
+
const getRootEntity = (element) => {
|
|
41
|
+
let entity = element.parent;
|
|
42
|
+
while (entity.kind !== "entity") entity = entity.parent;
|
|
43
|
+
return entity;
|
|
44
|
+
};
|
|
42
45
|
|
|
43
46
|
const _isDataSubject = (element, target) => {
|
|
44
47
|
return (
|
|
45
48
|
!element.isAssociation &&
|
|
46
|
-
element[
|
|
47
|
-
target[
|
|
48
|
-
)
|
|
49
|
-
}
|
|
49
|
+
element["@PersonalData.FieldSemantics"] === "DataSubjectID" &&
|
|
50
|
+
target["@PersonalData.EntitySemantics"] === "DataSubject"
|
|
51
|
+
);
|
|
52
|
+
};
|
|
50
53
|
|
|
51
|
-
const getPick = event => {
|
|
54
|
+
const getPick = (event) => {
|
|
52
55
|
return (element, target) => {
|
|
53
|
-
if (!hasPersonalData(target)) return
|
|
54
|
-
const categories = []
|
|
55
|
-
if (!element.isAssociation && element.key) categories.push(
|
|
56
|
-
if (_isDataSubject(element, target)) categories.push(
|
|
57
|
-
if (event in WRITE && element[
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
}
|
|
56
|
+
if (!hasPersonalData(target)) return;
|
|
57
|
+
const categories = [];
|
|
58
|
+
if (!element.isAssociation && element.key) categories.push("ObjectID");
|
|
59
|
+
if (_isDataSubject(element, target)) categories.push("DataSubjectID");
|
|
60
|
+
if (event in WRITE && element["@PersonalData.IsPotentiallyPersonal"])
|
|
61
|
+
categories.push("IsPotentiallyPersonal");
|
|
62
|
+
if (element["@PersonalData.IsPotentiallySensitive"]) categories.push("IsPotentiallySensitive");
|
|
63
|
+
if (categories.length) return { categories };
|
|
64
|
+
};
|
|
65
|
+
};
|
|
62
66
|
|
|
63
67
|
const _getHash = (entity, row) => {
|
|
64
68
|
return `${entity.name}(${Object.keys(entity.keys)
|
|
65
|
-
.map(k => `${k}=${row[k]}`)
|
|
66
|
-
.join(
|
|
67
|
-
}
|
|
69
|
+
.map((k) => `${k}=${row[k]}`)
|
|
70
|
+
.join(",")})`;
|
|
71
|
+
};
|
|
68
72
|
|
|
69
73
|
const createLogEntry = (logs, entity, row) => {
|
|
70
|
-
const hash = _getHash(entity, row)
|
|
71
|
-
let log = logs[hash]
|
|
74
|
+
const hash = _getHash(entity, row);
|
|
75
|
+
let log = logs[hash];
|
|
72
76
|
if (!log) {
|
|
73
77
|
logs[hash] = {
|
|
74
|
-
data_subject: { id: {}, role: entity[
|
|
78
|
+
data_subject: { id: {}, role: entity["@PersonalData.DataSubjectRole"] },
|
|
75
79
|
object: { type: entity.name, id: {} },
|
|
76
80
|
attributes: []
|
|
77
|
-
}
|
|
78
|
-
log = logs[hash]
|
|
81
|
+
};
|
|
82
|
+
log = logs[hash];
|
|
79
83
|
}
|
|
80
|
-
return log
|
|
81
|
-
}
|
|
84
|
+
return log;
|
|
85
|
+
};
|
|
82
86
|
|
|
83
87
|
const addObjectID = (log, row, key) => {
|
|
84
|
-
if (!(key in log.object.id) && key !==
|
|
85
|
-
|
|
88
|
+
if (!(key in log.object.id) && key !== "IsActiveEntity")
|
|
89
|
+
log.object.id[key] = row[key] || row._old?.[key];
|
|
90
|
+
};
|
|
86
91
|
|
|
87
92
|
const addDataSubject = (log, row, key, entity) => {
|
|
88
|
-
if (!log.data_subject.type) log.data_subject.type = entity.name
|
|
93
|
+
if (!log.data_subject.type) log.data_subject.type = entity.name;
|
|
89
94
|
if (!(key in log.data_subject.id)) {
|
|
90
|
-
const value = row[key] || row._old?.[key]
|
|
91
|
-
log.data_subject.id[key] = value
|
|
95
|
+
const value = row[key] || row._old?.[key];
|
|
96
|
+
log.data_subject.id[key] = value;
|
|
92
97
|
}
|
|
93
|
-
}
|
|
98
|
+
};
|
|
94
99
|
|
|
95
100
|
const _addKeysToWhere = (keys, row, alias) => {
|
|
96
101
|
return keys
|
|
97
|
-
.filter(key => !key.isAssociation && key.name !==
|
|
102
|
+
.filter((key) => !key.isAssociation && key.name !== "IsActiveEntity")
|
|
98
103
|
.reduce((keys, key) => {
|
|
99
|
-
if (keys.length) keys.push(
|
|
100
|
-
keys.push({ ref: [alias, key.name] },
|
|
101
|
-
return keys
|
|
102
|
-
}, [])
|
|
103
|
-
}
|
|
104
|
+
if (keys.length) keys.push("and");
|
|
105
|
+
keys.push({ ref: [alias, key.name] }, "=", { val: row[key.name] || row._old?.[key.name] });
|
|
106
|
+
return keys;
|
|
107
|
+
}, []);
|
|
108
|
+
};
|
|
104
109
|
|
|
105
110
|
const _keyColumns = (keys, alias) => {
|
|
106
111
|
return keys
|
|
107
|
-
.filter(key => !key.isAssociation && key.name !==
|
|
108
|
-
.map(key => ({ ref: [alias, key.name] }))
|
|
109
|
-
}
|
|
112
|
+
.filter((key) => !key.isAssociation && key.name !== "IsActiveEntity")
|
|
113
|
+
.map((key) => ({ ref: [alias, key.name] }));
|
|
114
|
+
};
|
|
110
115
|
|
|
111
|
-
const _alias = entity => {
|
|
116
|
+
const _alias = (entity) => {
|
|
112
117
|
// 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}.`,
|
|
115
|
-
}
|
|
118
|
+
if (!entity._service) return `${entity}`;
|
|
119
|
+
return entity.name.replace(`${entity._service.name}.`, "").replace(/\./g, "_");
|
|
120
|
+
};
|
|
116
121
|
|
|
117
122
|
const _buildSubSelect = (model, { entity, relative, element, next }, row, previousCqn) => {
|
|
118
123
|
// relative is a parent or an entity itself
|
|
119
124
|
|
|
120
|
-
const keys = Object.values(entity.keys)
|
|
125
|
+
const keys = Object.values(entity.keys);
|
|
121
126
|
|
|
122
|
-
const entityName = entity.name
|
|
123
|
-
const as = _alias(entity)
|
|
127
|
+
const entityName = entity.name;
|
|
128
|
+
const as = _alias(entity);
|
|
124
129
|
|
|
125
|
-
const childCqn = SELECT.from({ ref: [entityName], as }).columns(_keyColumns(keys, as))
|
|
130
|
+
const childCqn = SELECT.from({ ref: [entityName], as }).columns(_keyColumns(keys, as));
|
|
126
131
|
|
|
127
|
-
const targetAlias = _alias(element._target)
|
|
128
|
-
const relativeAlias = _alias(relative)
|
|
132
|
+
const targetAlias = _alias(element._target);
|
|
133
|
+
const relativeAlias = _alias(relative);
|
|
129
134
|
|
|
130
135
|
// REVISIT: there seems to be a caching issue in cds^9 when elemets are renamed
|
|
131
|
-
if (!(
|
|
132
|
-
const newRelation = Relation.to(relative)
|
|
133
|
-
relative._relations = new Proxy(exposeRelation(newRelation), relationHandler(newRelation))
|
|
136
|
+
if (!("_relations" in relative) || !relative._relations[element.name]) {
|
|
137
|
+
const newRelation = Relation.to(relative);
|
|
138
|
+
relative._relations = new Proxy(exposeRelation(newRelation), relationHandler(newRelation));
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
let w = relative._relations[element.name].join(targetAlias, relativeAlias)
|
|
141
|
+
let w = relative._relations[element.name].join(targetAlias, relativeAlias);
|
|
137
142
|
|
|
138
143
|
// REVISIT: rewrite to path expression, if alias for relative is already used in subselect to avoid sql error
|
|
139
144
|
if (previousCqn?._aliases.has(relativeAlias)) {
|
|
140
|
-
let t
|
|
141
|
-
for (const a in entity.associations)
|
|
142
|
-
|
|
145
|
+
let t;
|
|
146
|
+
for (const a in entity.associations)
|
|
147
|
+
if (entity.associations[a].target === relative.name) t = entity.associations[a];
|
|
148
|
+
if (t && w[0]?.xpr)
|
|
149
|
+
for (const ele of w[0].xpr)
|
|
150
|
+
if (ele.ref?.[0] === relativeAlias) ele.ref.splice(0, 1, as, t.name);
|
|
143
151
|
}
|
|
144
|
-
childCqn._aliases = new Set(previousCqn ? [...previousCqn._aliases.values(), as] : [as])
|
|
152
|
+
childCqn._aliases = new Set(previousCqn ? [...previousCqn._aliases.values(), as] : [as]);
|
|
145
153
|
|
|
146
|
-
childCqn.where(w)
|
|
154
|
+
childCqn.where(w);
|
|
147
155
|
|
|
148
|
-
if (previousCqn) childCqn.where(
|
|
149
|
-
else childCqn.where(_addKeysToWhere(keys, row, as))
|
|
156
|
+
if (previousCqn) childCqn.where("exists", previousCqn);
|
|
157
|
+
else childCqn.where(_addKeysToWhere(keys, row, as));
|
|
150
158
|
|
|
151
|
-
if (next) return _buildSubSelect(model, next, {}, childCqn)
|
|
159
|
+
if (next) return _buildSubSelect(model, next, {}, childCqn);
|
|
152
160
|
|
|
153
|
-
return childCqn
|
|
154
|
-
}
|
|
161
|
+
return childCqn;
|
|
162
|
+
};
|
|
155
163
|
|
|
156
164
|
const _getDataSubjectIdQuery = ({ dataSubjectEntity, subs }, row, model) => {
|
|
157
|
-
const keys = Object.values(dataSubjectEntity.keys)
|
|
158
|
-
const as = _alias(dataSubjectEntity)
|
|
165
|
+
const keys = Object.values(dataSubjectEntity.keys);
|
|
166
|
+
const as = _alias(dataSubjectEntity);
|
|
159
167
|
|
|
160
168
|
const cqn = SELECT.one
|
|
161
169
|
.from({ ref: [dataSubjectEntity.name], as })
|
|
162
170
|
.columns(_keyColumns(keys, as))
|
|
163
|
-
.where([
|
|
171
|
+
.where(["exists", _buildSubSelect(model, subs[0], row)]);
|
|
164
172
|
|
|
165
173
|
// entity reused in different branches => must check all
|
|
166
|
-
for (let i = 1; i < subs.length; i++) cqn.or([
|
|
174
|
+
for (let i = 1; i < subs.length; i++) cqn.or(["exists", _buildSubSelect(model, subs[i], row)]);
|
|
167
175
|
|
|
168
|
-
return cqn
|
|
169
|
-
}
|
|
176
|
+
return cqn;
|
|
177
|
+
};
|
|
170
178
|
|
|
171
179
|
const _getUps = (entity, model) => {
|
|
172
180
|
if (entity.own($parents) == null) {
|
|
173
|
-
const ups = []
|
|
181
|
+
const ups = [];
|
|
174
182
|
for (const def of Object.values(model.definitions)) {
|
|
175
|
-
if (def.kind !==
|
|
183
|
+
if (def.kind !== "entity" || !def.associations) continue;
|
|
176
184
|
for (const element of Object.values(def.associations)) {
|
|
177
|
-
if (
|
|
178
|
-
|
|
185
|
+
if (
|
|
186
|
+
element.target !== entity.name ||
|
|
187
|
+
element._isBacklink ||
|
|
188
|
+
element.name === "SiblingEntity"
|
|
189
|
+
)
|
|
190
|
+
continue;
|
|
191
|
+
ups.push(element);
|
|
179
192
|
}
|
|
180
193
|
}
|
|
181
|
-
entity.set($parents, ups)
|
|
194
|
+
entity.set($parents, ups);
|
|
182
195
|
}
|
|
183
|
-
return entity.own($parents)
|
|
184
|
-
}
|
|
196
|
+
return entity.own($parents);
|
|
197
|
+
};
|
|
185
198
|
|
|
186
199
|
const _getDataSubjectUp = (root, model, entity, prev, next, result) => {
|
|
187
200
|
for (const element of _getUps(entity, model)) {
|
|
188
201
|
// cycle detection
|
|
189
|
-
if (element.own($visitedUp) == null) element.set($visitedUp, new Set())
|
|
190
|
-
if (element.own($visitedUp).has(root)) continue
|
|
191
|
-
element.own($visitedUp).add(root)
|
|
192
|
-
|
|
193
|
-
const me = { entity, relative: element.parent, element }
|
|
194
|
-
if (prev) prev.next = me
|
|
195
|
-
if (element.parent[
|
|
196
|
-
if (!result) result = { dataSubjectEntity: element.parent, subs: [] }
|
|
197
|
-
result.subs.push(next || me)
|
|
198
|
-
return result
|
|
202
|
+
if (element.own($visitedUp) == null) element.set($visitedUp, new Set());
|
|
203
|
+
if (element.own($visitedUp).has(root)) continue;
|
|
204
|
+
element.own($visitedUp).add(root);
|
|
205
|
+
|
|
206
|
+
const me = { entity, relative: element.parent, element };
|
|
207
|
+
if (prev) prev.next = me;
|
|
208
|
+
if (element.parent["@PersonalData.EntitySemantics"] === "DataSubject") {
|
|
209
|
+
if (!result) result = { dataSubjectEntity: element.parent, subs: [] };
|
|
210
|
+
result.subs.push(next || me);
|
|
211
|
+
return result;
|
|
199
212
|
} else {
|
|
200
213
|
// dfs is a must here
|
|
201
|
-
result = _getDataSubjectUp(root, model, element.parent, me, next || me, result)
|
|
214
|
+
result = _getDataSubjectUp(root, model, element.parent, me, next || me, result);
|
|
202
215
|
}
|
|
203
216
|
}
|
|
204
|
-
return result
|
|
205
|
-
}
|
|
217
|
+
return result;
|
|
218
|
+
};
|
|
206
219
|
|
|
207
220
|
const _getDataSubjectDown = (root, entity, prev, next) => {
|
|
208
|
-
const associations = Object.values(entity.associations || {}).filter(e => !e._isBacklink)
|
|
221
|
+
const associations = Object.values(entity.associations || {}).filter((e) => !e._isBacklink);
|
|
209
222
|
// bfs makes more sense here -> check all own assocs first before going deeper
|
|
210
223
|
for (const element of associations) {
|
|
211
|
-
const me = { entity, relative: entity, element }
|
|
212
|
-
if (element._target[
|
|
213
|
-
if (prev) prev.next = me
|
|
214
|
-
return { dataSubjectEntity: element._target, subs: [next || me] }
|
|
224
|
+
const me = { entity, relative: entity, element };
|
|
225
|
+
if (element._target["@PersonalData.EntitySemantics"] === "DataSubject") {
|
|
226
|
+
if (prev) prev.next = me;
|
|
227
|
+
return { dataSubjectEntity: element._target, subs: [next || me] };
|
|
215
228
|
}
|
|
216
229
|
}
|
|
217
230
|
for (const element of associations) {
|
|
218
231
|
// cycle detection
|
|
219
|
-
if (element.own($visitedDown) == null) element.set($visitedDown, new Set())
|
|
220
|
-
if (element.own($visitedDown).has(root)) continue
|
|
221
|
-
element.own($visitedDown).add(root)
|
|
222
|
-
|
|
223
|
-
const me = { entity, relative: entity, element }
|
|
224
|
-
if (prev) prev.next = me
|
|
225
|
-
const dataSubject = _getDataSubjectDown(root, element._target, me, next || me)
|
|
226
|
-
if (dataSubject) return dataSubject
|
|
232
|
+
if (element.own($visitedDown) == null) element.set($visitedDown, new Set());
|
|
233
|
+
if (element.own($visitedDown).has(root)) continue;
|
|
234
|
+
element.own($visitedDown).add(root);
|
|
235
|
+
|
|
236
|
+
const me = { entity, relative: entity, element };
|
|
237
|
+
if (prev) prev.next = me;
|
|
238
|
+
const dataSubject = _getDataSubjectDown(root, element._target, me, next || me);
|
|
239
|
+
if (dataSubject) return dataSubject;
|
|
227
240
|
}
|
|
228
|
-
}
|
|
241
|
+
};
|
|
229
242
|
|
|
230
243
|
const getDataSubject = (entity, model) => {
|
|
231
244
|
if (entity.own($dataSubject) == null) {
|
|
232
245
|
// entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
|
|
233
246
|
// be always below or always above 'DataSubject' entity in CSN tree
|
|
234
|
-
let dataSubjectInfo = _getDataSubjectUp(entity.name, model, entity)
|
|
235
|
-
if (!dataSubjectInfo) dataSubjectInfo = _getDataSubjectDown(entity.name, entity)
|
|
236
|
-
entity.set($dataSubject, dataSubjectInfo)
|
|
247
|
+
let dataSubjectInfo = _getDataSubjectUp(entity.name, model, entity);
|
|
248
|
+
if (!dataSubjectInfo) dataSubjectInfo = _getDataSubjectDown(entity.name, entity);
|
|
249
|
+
entity.set($dataSubject, dataSubjectInfo);
|
|
237
250
|
}
|
|
238
|
-
return entity.own($dataSubject)
|
|
239
|
-
}
|
|
251
|
+
return entity.own($dataSubject);
|
|
252
|
+
};
|
|
240
253
|
|
|
241
|
-
const _getDataSubjectsMap = req => {
|
|
242
|
-
const mapKey = getMapKeyForCurrentRequest(req)
|
|
243
|
-
const _audit = (req.context._audit ??= {})
|
|
244
|
-
if (!_audit.dataSubjects) _audit.dataSubjects = new Map()
|
|
245
|
-
if (!_audit.dataSubjects.has(mapKey)) _audit.dataSubjects.set(mapKey, new Map())
|
|
246
|
-
return _audit.dataSubjects.get(mapKey)
|
|
247
|
-
}
|
|
254
|
+
const _getDataSubjectsMap = (req) => {
|
|
255
|
+
const mapKey = getMapKeyForCurrentRequest(req);
|
|
256
|
+
const _audit = (req.context._audit ??= {});
|
|
257
|
+
if (!_audit.dataSubjects) _audit.dataSubjects = new Map();
|
|
258
|
+
if (!_audit.dataSubjects.has(mapKey)) _audit.dataSubjects.set(mapKey, new Map());
|
|
259
|
+
return _audit.dataSubjects.get(mapKey);
|
|
260
|
+
};
|
|
248
261
|
|
|
249
262
|
const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => {
|
|
250
|
-
const dataSubjectInfo = getDataSubject(entity, model)
|
|
251
|
-
const role = dataSubjectInfo.dataSubjectEntity[
|
|
252
|
-
log.data_subject.role ??= role
|
|
253
|
-
log.data_subject.type = dataSubjectInfo.dataSubjectEntity.name
|
|
263
|
+
const dataSubjectInfo = getDataSubject(entity, model);
|
|
264
|
+
const role = dataSubjectInfo.dataSubjectEntity["@PersonalData.DataSubjectRole"];
|
|
265
|
+
log.data_subject.role ??= role;
|
|
266
|
+
log.data_subject.type = dataSubjectInfo.dataSubjectEntity.name;
|
|
254
267
|
/*
|
|
255
268
|
* for each req (cf. $batch with atomicity) and data subject role (e.g., customer vs supplier),
|
|
256
269
|
* store (in audit data structure at context) and reuse a single promise to look up the respective data subject
|
|
257
270
|
*/
|
|
258
|
-
const map = _getDataSubjectsMap(req)
|
|
259
|
-
if (map.has(role)) log.data_subject.id = map.get(role)
|
|
271
|
+
const map = _getDataSubjectsMap(req);
|
|
272
|
+
if (map.has(role)) log.data_subject.id = map.get(role);
|
|
260
273
|
// REVISIT by downward lookups row might already contain ID - some potential to optimize
|
|
261
|
-
else map.set(role, _getDataSubjectIdQuery(dataSubjectInfo, row, model))
|
|
262
|
-
}
|
|
274
|
+
else map.set(role, _getDataSubjectIdQuery(dataSubjectInfo, row, model));
|
|
275
|
+
};
|
|
263
276
|
|
|
264
277
|
const resolveDataSubjects = (logs, req) => {
|
|
265
|
-
const ps = []
|
|
278
|
+
const ps = [];
|
|
266
279
|
|
|
267
|
-
const map = _getDataSubjectsMap(req)
|
|
280
|
+
const map = _getDataSubjectsMap(req);
|
|
268
281
|
|
|
269
282
|
for (const each of Object.values(logs)) {
|
|
270
283
|
if (each.data_subject.id instanceof cds.ql.Query) {
|
|
271
|
-
const q = each.data_subject.id
|
|
284
|
+
const q = each.data_subject.id;
|
|
272
285
|
if (!map.has(q)) {
|
|
273
|
-
const p = cds.run(q).then(res => map.set(q, res))
|
|
274
|
-
map.set(q, p)
|
|
275
|
-
ps.push(p)
|
|
286
|
+
const p = cds.run(q).then((res) => map.set(q, res));
|
|
287
|
+
map.set(q, p);
|
|
288
|
+
ps.push(p);
|
|
276
289
|
}
|
|
277
290
|
}
|
|
278
291
|
}
|
|
@@ -280,11 +293,40 @@ const resolveDataSubjects = (logs, req) => {
|
|
|
280
293
|
return Promise.all(ps).then(() => {
|
|
281
294
|
for (const each of Object.values(logs)) {
|
|
282
295
|
if (each.data_subject.id instanceof cds.ql.Query) {
|
|
283
|
-
each.data_subject.id = map.get(each.data_subject.id)
|
|
296
|
+
each.data_subject.id = map.get(each.data_subject.id);
|
|
284
297
|
}
|
|
285
298
|
}
|
|
286
|
-
})
|
|
287
|
-
}
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const getAppMetadata = () => {
|
|
303
|
+
const appMetadata = cds.env.app;
|
|
304
|
+
|
|
305
|
+
if (appMetadata) {
|
|
306
|
+
return {
|
|
307
|
+
appID: appMetadata.id,
|
|
308
|
+
appName: appMetadata.name,
|
|
309
|
+
appURL: appMetadata.url,
|
|
310
|
+
organization_name: appMetadata.organization_name,
|
|
311
|
+
space_name: appMetadata.space_name
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// fallback: if the app metadata is undefined, then extract the metadata from the underlying environment (CF/Kyma/...)
|
|
316
|
+
const vcapApplication = process.env.VCAP_APPLICATION && JSON.parse(process.env.VCAP_APPLICATION);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
appID: vcapApplication && vcapApplication.application_id,
|
|
320
|
+
appName: vcapApplication && vcapApplication.application_name,
|
|
321
|
+
organization_name: vcapApplication && vcapApplication.organization_name,
|
|
322
|
+
space_name: vcapApplication && vcapApplication.space_name,
|
|
323
|
+
appURL:
|
|
324
|
+
vcapApplication &&
|
|
325
|
+
vcapApplication.application_uris &&
|
|
326
|
+
vcapApplication.application_uris[0] &&
|
|
327
|
+
`https://${vcapApplication.application_uris[0].replace(/^https?:\/\//, "")}`
|
|
328
|
+
};
|
|
329
|
+
};
|
|
288
330
|
|
|
289
331
|
module.exports = {
|
|
290
332
|
hasPersonalData,
|
|
@@ -295,5 +337,6 @@ module.exports = {
|
|
|
295
337
|
addObjectID,
|
|
296
338
|
addDataSubject,
|
|
297
339
|
addDataSubjectForDetailsEntity,
|
|
298
|
-
resolveDataSubjects
|
|
299
|
-
|
|
340
|
+
resolveDataSubjects,
|
|
341
|
+
appMetadata: getAppMetadata()
|
|
342
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/audit-logging",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
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)",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"lint": "npx eslint . --max-warnings=0",
|
|
16
|
-
"
|
|
16
|
+
"format": "npx prettier . --write && format-cds -f",
|
|
17
|
+
"format:check": "npx prettier . --check && format-cds --check",
|
|
18
|
+
"test": "npx jest --silent --testPathIgnorePatterns=integration"
|
|
17
19
|
},
|
|
18
20
|
"peerDependencies": {
|
|
19
21
|
"@sap/cds": ">=8"
|
|
@@ -22,12 +24,10 @@
|
|
|
22
24
|
"@cap-js/audit-logging": "file:.",
|
|
23
25
|
"@cap-js/cds-test": ">=0",
|
|
24
26
|
"@cap-js/sqlite": ">=1",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
+
"@sap/cds-lsp": "^9.9.0",
|
|
28
|
+
"@sap/cds-mtxs": "^3.7.1",
|
|
27
29
|
"express": "^4",
|
|
28
|
-
"jest": "^
|
|
29
|
-
"pretty-quick": "^4.2.2",
|
|
30
|
-
"simple-git-hooks": "^2.13.1"
|
|
30
|
+
"jest": "^30"
|
|
31
31
|
},
|
|
32
32
|
"cds": {
|
|
33
33
|
"requires": {
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"kinds": {
|
|
51
|
-
"audit-log-to-console": {
|
|
52
|
-
"impl": "@cap-js/audit-logging/srv/log2console"
|
|
53
|
-
},
|
|
54
51
|
"audit-log-to-als": {
|
|
55
52
|
"impl": "@cap-js/audit-logging/srv/log2als"
|
|
56
53
|
},
|
|
54
|
+
"audit-log-to-console": {
|
|
55
|
+
"impl": "@cap-js/audit-logging/srv/log2console"
|
|
56
|
+
},
|
|
57
57
|
"audit-log-to-restv2": {
|
|
58
58
|
"impl": "@cap-js/audit-logging/srv/log2restv2",
|
|
59
59
|
"vcap": {
|
|
@@ -61,7 +61,10 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"audit-log-to-alsng": {
|
|
64
|
-
"impl": "@cap-js/audit-logging/srv/log2alsng"
|
|
64
|
+
"impl": "@cap-js/audit-logging/srv/log2alsng",
|
|
65
|
+
"vcap": {
|
|
66
|
+
"name": "auditlog-ng"
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
}
|
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");
|