@cap-js/audit-logging 1.2.0 → 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 +9 -13
- package/lib/_relation.js +103 -100
- package/lib/access.js +38 -37
- package/lib/modification.js +117 -108
- package/lib/utils.js +172 -160
- package/package.json +6 -7
- package/srv/log2alsng.js +101 -98
- package/srv/log2console.js +7 -7
- package/srv/log2restv2.js +19 -38
- package/srv/service.js +15 -15
package/lib/modification.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const cds = require(
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
2
|
|
|
3
3
|
// REVISIT: don't require internal stuff
|
|
4
|
-
const getTemplate = require(
|
|
4
|
+
const getTemplate = require("@sap/cds/libx/_runtime/common/utils/template");
|
|
5
5
|
|
|
6
6
|
const {
|
|
7
7
|
getMapKeyForCurrentRequest,
|
|
@@ -12,58 +12,58 @@ const {
|
|
|
12
12
|
addDataSubject,
|
|
13
13
|
addDataSubjectForDetailsEntity,
|
|
14
14
|
resolveDataSubjects
|
|
15
|
-
} = require(
|
|
15
|
+
} = require("./utils");
|
|
16
16
|
|
|
17
|
-
let audit
|
|
17
|
+
let audit;
|
|
18
18
|
|
|
19
19
|
const _applyTransitionRecursively = (transition, data, d, old = {}) => {
|
|
20
20
|
for (const [k, v] of transition.mapping) {
|
|
21
21
|
if (v.transition) {
|
|
22
|
-
const ok = v.ref?.[0] || k
|
|
23
|
-
if (!data[ok]) continue
|
|
22
|
+
const ok = v.ref?.[0] || k;
|
|
23
|
+
if (!data[ok]) continue;
|
|
24
24
|
if (Array.isArray(data[ok])) {
|
|
25
|
-
d[k] = []
|
|
25
|
+
d[k] = [];
|
|
26
26
|
for (let i = 0; i < data[ok].length; i++) {
|
|
27
|
-
const _op = data[ok][i]._op || d._op
|
|
28
|
-
d[k].push(_op ? { _op } : {})
|
|
29
|
-
const _old = old[ok]?.[i] || data[ok][i]._old
|
|
30
|
-
if (_old) d[k][i]._old = _old
|
|
31
|
-
_applyTransitionRecursively(v.transition, data[ok][i], d[k][i], _old)
|
|
27
|
+
const _op = data[ok][i]._op || d._op;
|
|
28
|
+
d[k].push(_op ? { _op } : {});
|
|
29
|
+
const _old = old[ok]?.[i] || data[ok][i]._old;
|
|
30
|
+
if (_old) d[k][i]._old = _old;
|
|
31
|
+
_applyTransitionRecursively(v.transition, data[ok][i], d[k][i], _old);
|
|
32
32
|
}
|
|
33
33
|
if (old[ok] && data[ok].length !== old[ok].length) {
|
|
34
|
-
for (const each of Object.values(old[ok]).filter(ele => !ele.__visited)) {
|
|
35
|
-
const i = d[k].push({ _op:
|
|
36
|
-
d[k][i - 1]._old = each
|
|
37
|
-
_applyTransitionRecursively(v.transition, each, d[k][i - 1], each)
|
|
34
|
+
for (const each of Object.values(old[ok]).filter((ele) => !ele.__visited)) {
|
|
35
|
+
const i = d[k].push({ _op: "delete" });
|
|
36
|
+
d[k][i - 1]._old = each;
|
|
37
|
+
_applyTransitionRecursively(v.transition, each, d[k][i - 1], each);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
} else {
|
|
41
|
-
d[k] = { _op: data[ok]._op || d._op }
|
|
42
|
-
const _old = old[ok] || data[ok]._old
|
|
43
|
-
if (_old) d[k]._old = _old
|
|
44
|
-
_applyTransitionRecursively(v.transition, data[ok], d[k], _old)
|
|
41
|
+
d[k] = { _op: data[ok]._op || d._op };
|
|
42
|
+
const _old = old[ok] || data[ok]._old;
|
|
43
|
+
if (_old) d[k]._old = _old;
|
|
44
|
+
_applyTransitionRecursively(v.transition, data[ok], d[k], _old);
|
|
45
45
|
}
|
|
46
46
|
} else if (v.ref) {
|
|
47
|
-
if (v.ref[0] in data) d[k] = data[v.ref[0]]
|
|
48
|
-
if (v.ref[0] in old) d._old[k] = old[v.ref[0]]
|
|
47
|
+
if (v.ref[0] in data) d[k] = data[v.ref[0]];
|
|
48
|
+
if (v.ref[0] in old) d._old[k] = old[v.ref[0]];
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
if (Object.keys(old).length) old.__visited = true
|
|
52
|
-
}
|
|
51
|
+
if (Object.keys(old).length) old.__visited = true;
|
|
52
|
+
};
|
|
53
53
|
|
|
54
54
|
// REVISIT: remove once old database impl is removed
|
|
55
55
|
const _getDataWithAppliedTransitions = (data, req) => {
|
|
56
|
-
let d
|
|
57
|
-
const query = req.query.INSERT || req.query.UPDATE || req.query.DELETE
|
|
56
|
+
let d;
|
|
57
|
+
const query = req.query.INSERT || req.query.UPDATE || req.query.DELETE;
|
|
58
58
|
// NOTE: there will only be transitions if old database impl is used
|
|
59
|
-
const transition = query._transitions?.find(t => t.queryTarget.name === req.target.name)
|
|
59
|
+
const transition = query._transitions?.find((t) => t.queryTarget.name === req.target.name);
|
|
60
60
|
if (transition) {
|
|
61
|
-
d = data._op ? { _op: data._op } : {}
|
|
62
|
-
if (data._old) d._old = {}
|
|
63
|
-
_applyTransitionRecursively(transition, data, d, data._old)
|
|
61
|
+
d = data._op ? { _op: data._op } : {};
|
|
62
|
+
if (data._old) d._old = {};
|
|
63
|
+
_applyTransitionRecursively(transition, data, d, data._old);
|
|
64
64
|
}
|
|
65
|
-
return d || data
|
|
66
|
-
}
|
|
65
|
+
return d || data;
|
|
66
|
+
};
|
|
67
67
|
|
|
68
68
|
/*
|
|
69
69
|
* REVISIT: diff() doesn't work in srv after phase but foreign key propagation has not yet taken place in srv before phase
|
|
@@ -72,88 +72,90 @@ const _getDataWithAppliedTransitions = (data, req) => {
|
|
|
72
72
|
*/
|
|
73
73
|
const addDiffToCtx = async function (req) {
|
|
74
74
|
// store diff in audit data structure at context
|
|
75
|
-
const _audit = (req.context._audit ??= {})
|
|
76
|
-
if (!_audit.diffs) _audit.diffs = new Map()
|
|
75
|
+
const _audit = (req.context._audit ??= {});
|
|
76
|
+
if (!_audit.diffs) _audit.diffs = new Map();
|
|
77
77
|
|
|
78
78
|
// get diff
|
|
79
|
-
let diff = (await req.diff()) || {}
|
|
80
|
-
diff = _getDataWithAppliedTransitions(diff, req)
|
|
79
|
+
let diff = (await req.diff()) || {};
|
|
80
|
+
diff = _getDataWithAppliedTransitions(diff, req);
|
|
81
81
|
|
|
82
82
|
// add keys, if necessary
|
|
83
|
-
let keys = _getDataWithAppliedTransitions(Object.assign({}, req.data), req)
|
|
84
|
-
for (const key in keys) if (!(key in req.target.keys)) delete keys[key]
|
|
85
|
-
Object.assign(diff, keys)
|
|
83
|
+
let keys = _getDataWithAppliedTransitions(Object.assign({}, req.data), req);
|
|
84
|
+
for (const key in keys) if (!(key in req.target.keys)) delete keys[key];
|
|
85
|
+
Object.assign(diff, keys);
|
|
86
86
|
|
|
87
|
-
_audit.diffs.set(req._.query, diff)
|
|
88
|
-
}
|
|
89
|
-
addDiffToCtx._initial = true
|
|
87
|
+
_audit.diffs.set(req._.query, diff);
|
|
88
|
+
};
|
|
89
|
+
addDiffToCtx._initial = true;
|
|
90
90
|
|
|
91
91
|
const _getOldAndNew = (action, row, key, entity) => {
|
|
92
|
-
let oldValue = action ===
|
|
93
|
-
if (oldValue === undefined)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
92
|
+
let oldValue = action === "Create" ? null : row._old && row._old[key];
|
|
93
|
+
if (oldValue === undefined)
|
|
94
|
+
oldValue = action === "Update" && key in entity.keys ? row[key] : null;
|
|
95
|
+
else if (Array.isArray(oldValue)) oldValue = JSON.stringify(oldValue);
|
|
96
|
+
let newValue = action === "Delete" ? null : row[key];
|
|
97
|
+
if (newValue === undefined) newValue = null;
|
|
98
|
+
else if (Array.isArray(newValue)) newValue = JSON.stringify(newValue);
|
|
99
|
+
return { oldValue, newValue };
|
|
100
|
+
};
|
|
100
101
|
|
|
101
102
|
const _addAttribute = (log, action, row, key, entity) => {
|
|
102
|
-
if (!log.attributes.find(ele => ele.name === key)) {
|
|
103
|
-
const { oldValue, newValue } = _getOldAndNew(action, row, key, entity)
|
|
103
|
+
if (!log.attributes.find((ele) => ele.name === key)) {
|
|
104
|
+
const { oldValue, newValue } = _getOldAndNew(action, row, key, entity);
|
|
104
105
|
if (oldValue !== newValue) {
|
|
105
|
-
const attr = { name: key }
|
|
106
|
-
if (action !==
|
|
107
|
-
if (action !==
|
|
108
|
-
log.attributes.push(attr)
|
|
106
|
+
const attr = { name: key };
|
|
107
|
+
if (action !== "Create") attr.old = oldValue;
|
|
108
|
+
if (action !== "Delete") attr.new = newValue;
|
|
109
|
+
log.attributes.push(attr);
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
}
|
|
112
|
+
};
|
|
112
113
|
|
|
113
114
|
const _maskAttribute = (attributes, key) => {
|
|
114
|
-
const attribute = attributes?.find(ele => ele.name === key)
|
|
115
|
+
const attribute = attributes?.find((ele) => ele.name === key);
|
|
115
116
|
if (attribute) {
|
|
116
|
-
if (
|
|
117
|
-
if (
|
|
117
|
+
if ("old" in attribute) attribute.old = "***";
|
|
118
|
+
if ("new" in attribute) attribute.new = "***";
|
|
118
119
|
}
|
|
119
|
-
}
|
|
120
|
+
};
|
|
120
121
|
|
|
121
|
-
const _processorFnModification = (modificationLogs, model, req, beforeWrite) => elementInfo => {
|
|
122
|
-
if (!elementInfo.row?._op) return
|
|
122
|
+
const _processorFnModification = (modificationLogs, model, req, beforeWrite) => (elementInfo) => {
|
|
123
|
+
if (!elementInfo.row?._op) return;
|
|
123
124
|
|
|
124
|
-
let { row, key, element, plain } = elementInfo
|
|
125
|
+
let { row, key, element, plain } = elementInfo;
|
|
125
126
|
|
|
126
127
|
// delete in before phase, create and update in after phase
|
|
127
|
-
if ((row._op ===
|
|
128
|
+
if ((row._op === "delete") !== !!beforeWrite) return;
|
|
128
129
|
|
|
129
|
-
const entity = getRootEntity(element)
|
|
130
|
-
const action = row._op[0].toUpperCase() + row._op.slice(1)
|
|
130
|
+
const entity = getRootEntity(element);
|
|
131
|
+
const action = row._op[0].toUpperCase() + row._op.slice(1);
|
|
131
132
|
|
|
132
133
|
// create or augment log entry
|
|
133
|
-
const modificationLog = createLogEntry(modificationLogs, entity, row)
|
|
134
|
+
const modificationLog = createLogEntry(modificationLogs, entity, row);
|
|
134
135
|
|
|
135
136
|
// process categories
|
|
136
137
|
for (const category of plain.categories) {
|
|
137
|
-
if (category ===
|
|
138
|
-
addObjectID(modificationLog, row, key)
|
|
139
|
-
} else if (category ===
|
|
140
|
-
addDataSubject(modificationLog, row, key, entity)
|
|
141
|
-
} else if (category ===
|
|
142
|
-
_addAttribute(modificationLog, action, row, key, entity)
|
|
138
|
+
if (category === "ObjectID") {
|
|
139
|
+
addObjectID(modificationLog, row, key);
|
|
140
|
+
} else if (category === "DataSubjectID") {
|
|
141
|
+
addDataSubject(modificationLog, row, key, entity);
|
|
142
|
+
} else if (category === "IsPotentiallyPersonal" || category === "IsPotentiallySensitive") {
|
|
143
|
+
_addAttribute(modificationLog, action, row, key, entity);
|
|
143
144
|
// do not log the value of a sensitive attribute
|
|
144
|
-
if (element[
|
|
145
|
+
if (element["@PersonalData.IsPotentiallySensitive"])
|
|
146
|
+
_maskAttribute(modificationLog.attributes, key);
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
// add promise to determine data subject if a DataSubjectDetails entity
|
|
149
151
|
if (
|
|
150
|
-
(entity[
|
|
151
|
-
entity[
|
|
152
|
+
(entity["@PersonalData.EntitySemantics"] === "DataSubjectDetails" ||
|
|
153
|
+
entity["@PersonalData.EntitySemantics"] === "Other") &&
|
|
152
154
|
Object.keys(modificationLog.data_subject.id).length === 0 // > id still an empty object -> promise not yet set
|
|
153
155
|
) {
|
|
154
|
-
addDataSubjectForDetailsEntity(row, modificationLog, req, entity, model)
|
|
156
|
+
addDataSubjectForDetailsEntity(row, modificationLog, req, entity, model);
|
|
155
157
|
}
|
|
156
|
-
}
|
|
158
|
+
};
|
|
157
159
|
|
|
158
160
|
const _getDataModificationLogs = (req, tx, diff, beforeWrite) => {
|
|
159
161
|
const template = getTemplate(
|
|
@@ -161,62 +163,69 @@ const _getDataModificationLogs = (req, tx, diff, beforeWrite) => {
|
|
|
161
163
|
Object.assign({ name: req.target._service.name, model: tx.model }),
|
|
162
164
|
req.target,
|
|
163
165
|
{ pick: getPick(req.event) }
|
|
164
|
-
)
|
|
166
|
+
);
|
|
165
167
|
|
|
166
|
-
const modificationLogs = {}
|
|
167
|
-
const processFn = _processorFnModification(modificationLogs, tx.model, req, beforeWrite)
|
|
168
|
+
const modificationLogs = {};
|
|
169
|
+
const processFn = _processorFnModification(modificationLogs, tx.model, req, beforeWrite);
|
|
168
170
|
// cds internal templating mechanism api changed in 8.2.0 -> polyfill
|
|
169
171
|
if (!template.process) {
|
|
170
|
-
module.exports._templateProcessor ??= require(
|
|
172
|
+
module.exports._templateProcessor ??= require("@sap/cds/libx/_runtime/common/utils/templateProcessor");
|
|
171
173
|
template.process = (data, processFn) => {
|
|
172
|
-
module.exports._templateProcessor({ processFn, row: data, template })
|
|
173
|
-
}
|
|
174
|
+
module.exports._templateProcessor({ processFn, row: data, template });
|
|
175
|
+
};
|
|
174
176
|
}
|
|
175
|
-
template.process(diff, processFn)
|
|
177
|
+
template.process(diff, processFn);
|
|
176
178
|
|
|
177
|
-
return modificationLogs
|
|
178
|
-
}
|
|
179
|
+
return modificationLogs;
|
|
180
|
+
};
|
|
179
181
|
|
|
180
182
|
const _calcModificationLogsHandler = async function (req, beforeWrite, that) {
|
|
181
|
-
const mapKey = getMapKeyForCurrentRequest(req)
|
|
183
|
+
const mapKey = getMapKeyForCurrentRequest(req);
|
|
182
184
|
|
|
183
|
-
const _audit = (req.context._audit ??= {})
|
|
184
|
-
const modificationLogs = _getDataModificationLogs(
|
|
185
|
+
const _audit = (req.context._audit ??= {});
|
|
186
|
+
const modificationLogs = _getDataModificationLogs(
|
|
187
|
+
req,
|
|
188
|
+
that,
|
|
189
|
+
_audit.diffs.get(mapKey),
|
|
190
|
+
beforeWrite
|
|
191
|
+
);
|
|
185
192
|
|
|
186
193
|
// store modificationLogs in audit data structure at context
|
|
187
|
-
if (!_audit.modificationLogs) _audit.modificationLogs = new Map()
|
|
188
|
-
const existingLogs = _audit.modificationLogs.get(mapKey) || {}
|
|
189
|
-
_audit.modificationLogs.set(mapKey, Object.assign(existingLogs, modificationLogs))
|
|
194
|
+
if (!_audit.modificationLogs) _audit.modificationLogs = new Map();
|
|
195
|
+
const existingLogs = _audit.modificationLogs.get(mapKey) || {};
|
|
196
|
+
_audit.modificationLogs.set(mapKey, Object.assign(existingLogs, modificationLogs));
|
|
190
197
|
|
|
191
198
|
// execute the data subject promises before going along to on phase
|
|
192
199
|
// guarantees that the reads are executed before the data is modified
|
|
193
|
-
await resolveDataSubjects(modificationLogs, req)
|
|
194
|
-
}
|
|
200
|
+
await resolveDataSubjects(modificationLogs, req);
|
|
201
|
+
};
|
|
195
202
|
|
|
196
203
|
const calcModLogs4Before = function (req) {
|
|
197
|
-
return _calcModificationLogsHandler(req, true, this)
|
|
198
|
-
}
|
|
204
|
+
return _calcModificationLogsHandler(req, true, this);
|
|
205
|
+
};
|
|
199
206
|
|
|
200
207
|
const calcModLogs4After = function (_, req) {
|
|
201
|
-
return _calcModificationLogsHandler(req, false, this)
|
|
202
|
-
}
|
|
208
|
+
return _calcModificationLogsHandler(req, false, this);
|
|
209
|
+
};
|
|
203
210
|
|
|
204
211
|
const emitModLogs = async function (_, req) {
|
|
205
|
-
const modificationLogs = req.context?._audit?.modificationLogs?.get(req.query)
|
|
206
|
-
if (!modificationLogs) return
|
|
212
|
+
const modificationLogs = req.context?._audit?.modificationLogs?.get(req.query);
|
|
213
|
+
if (!modificationLogs) return;
|
|
207
214
|
|
|
208
|
-
audit = audit || (await cds.connect.to(
|
|
215
|
+
audit = audit || (await cds.connect.to("audit-log"));
|
|
209
216
|
|
|
210
217
|
const modifications = Object.keys(modificationLogs)
|
|
211
|
-
.map(k => modificationLogs[k])
|
|
212
|
-
.filter(log => log.attributes.length)
|
|
218
|
+
.map((k) => modificationLogs[k])
|
|
219
|
+
.filter((log) => log.attributes.length);
|
|
213
220
|
|
|
214
|
-
await Promise.all(
|
|
215
|
-
|
|
221
|
+
await Promise.all(
|
|
222
|
+
modifications.map((modification) => audit.log("PersonalDataModified", modification))
|
|
223
|
+
);
|
|
224
|
+
};
|
|
216
225
|
|
|
217
226
|
module.exports = {
|
|
218
227
|
addDiffToCtx,
|
|
219
228
|
calcModLogs4Before,
|
|
220
229
|
calcModLogs4After,
|
|
221
230
|
emitModLogs
|
|
222
|
-
}
|
|
231
|
+
};
|