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