@cap-js/audit-logging 0.5.2 → 0.7.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/CHANGELOG.md +17 -0
- package/cds-plugin.js +14 -0
- package/lib/utils.js +44 -27
- package/package.json +1 -1
- package/srv/service.js +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## Version 0.7.0 - tbd
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Automatically promote entities that are associated with data subjects
|
|
12
|
+
|
|
13
|
+
## Version 0.6.0 - 2024-02-05
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Support for @sap/cds^7.5
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Automatic personal data modification logging for data subject details with renamed keys
|
|
22
|
+
- Data subject resolution in circular models
|
|
23
|
+
|
|
7
24
|
## Version 0.5.2 - 2023-12-08
|
|
8
25
|
|
|
9
26
|
### Fixed
|
package/cds-plugin.js
CHANGED
|
@@ -19,6 +19,20 @@ cds.on('served', services => {
|
|
|
19
19
|
for (const entity of service.entities) if (hasPersonalData(entity)) relevantEntities.push(entity)
|
|
20
20
|
if (!relevantEntities.length) continue
|
|
21
21
|
|
|
22
|
+
// automatically promote entities that are associated with data subjects
|
|
23
|
+
for (const entity of relevantEntities) {
|
|
24
|
+
if (entity['@PersonalData.EntitySemantics'] !== 'DataSubject') continue
|
|
25
|
+
for (const e of service.entities) {
|
|
26
|
+
for (const k in e.associations) {
|
|
27
|
+
if (e.associations[k].target === entity.name && k !== 'SiblingEntity') {
|
|
28
|
+
e['@PersonalData.EntitySemantics'] ??= 'Other'
|
|
29
|
+
e.associations[k]['@PersonalData.FieldSemantics'] ??= 'DataSubjectID'
|
|
30
|
+
if (!relevantEntities.includes(e)) relevantEntities.push(e)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
for (const entity of relevantEntities) {
|
|
23
37
|
/*
|
|
24
38
|
* data access
|
package/lib/utils.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
const WRITE = { CREATE: 1, UPDATE: 1, DELETE: 1 }
|
|
2
2
|
|
|
3
|
+
const $hasPersonalData = Symbol('@cap-js/audit-logging:hasPersonalData')
|
|
4
|
+
const $dataSubject = Symbol('@cap-js/audit-logging:dataSubject')
|
|
5
|
+
const $parents = Symbol('@cap-js/audit-logging:parents')
|
|
6
|
+
const $visitedUp = Symbol('@cap-js/audit-logging:visitedUp')
|
|
7
|
+
const $visitedDown = Symbol('@cap-js/audit-logging:visitedDown')
|
|
8
|
+
|
|
3
9
|
const hasPersonalData = entity => {
|
|
4
|
-
if (!entity
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
if (!entity.own($hasPersonalData)) {
|
|
11
|
+
if (!entity['@PersonalData.EntitySemantics']) entity.set($hasPersonalData, false)
|
|
12
|
+
else {
|
|
13
|
+
// default role to entity name
|
|
14
|
+
if (entity['@PersonalData.EntitySemantics'] === 'DataSubject' && !entity['@PersonalData.DataSubjectRole'])
|
|
15
|
+
entity['@PersonalData.DataSubjectRole'] = entity.name.match(/\w+/g).pop()
|
|
16
|
+
// prettier-ignore
|
|
17
|
+
const hasPersonalData = !!Object.values(entity.elements).some(element =>
|
|
18
|
+
element['@PersonalData.IsPotentiallyPersonal'] ||
|
|
19
|
+
element['@PersonalData.IsPotentiallySensitive'] ||
|
|
20
|
+
(element['@PersonalData.FieldSemantics'] && element['@PersonalData.FieldSemantics'] === 'DataSubjectID'))
|
|
21
|
+
entity.set($hasPersonalData, hasPersonalData)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return entity.own($hasPersonalData)
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
const getMapKeyForCurrentRequest = req => {
|
|
@@ -84,7 +93,7 @@ const _addKeysToWhere = (keys, row, alias) => {
|
|
|
84
93
|
.filter(key => !key.isAssociation && key.name !== 'IsActiveEntity')
|
|
85
94
|
.reduce((keys, key) => {
|
|
86
95
|
if (keys.length) keys.push('and')
|
|
87
|
-
keys.push({ ref: [alias, key.name] }, '=', { val: row[key.name] })
|
|
96
|
+
keys.push({ ref: [alias, key.name] }, '=', { val: row[key.name] || row._old?.[key.name] })
|
|
88
97
|
return keys
|
|
89
98
|
}, [])
|
|
90
99
|
}
|
|
@@ -136,21 +145,25 @@ const _getDataSubjectIdQuery = ({ dataSubjectEntity, subs }, row, model) => {
|
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
const _getUps = (entity, model) => {
|
|
139
|
-
if (entity.own(
|
|
148
|
+
if (entity.own($parents)) return entity[$parents]
|
|
140
149
|
const ups = []
|
|
141
150
|
for (const def of Object.values(model.definitions)) {
|
|
142
151
|
if (def.kind !== 'entity' || !def.associations) continue
|
|
143
152
|
for (const element of Object.values(def.associations)) {
|
|
144
|
-
if (element.target !== entity.name || element._isBacklink) continue
|
|
145
|
-
if (element.name === 'SiblingEntity') continue
|
|
153
|
+
if (element.target !== entity.name || element._isBacklink || element.name === 'SiblingEntity') continue
|
|
146
154
|
ups.push(element)
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
|
-
return entity.set(
|
|
157
|
+
return entity.set($parents, ups)
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
const _getDataSubjectUp = (model, entity, prev, next, result) => {
|
|
160
|
+
const _getDataSubjectUp = (root, model, entity, prev, next, result) => {
|
|
153
161
|
for (const element of _getUps(entity, model)) {
|
|
162
|
+
// cycle detection
|
|
163
|
+
if (!element.own($visitedUp)) element.set($visitedUp, new Set())
|
|
164
|
+
if (element.own($visitedUp).has(root)) continue
|
|
165
|
+
element.own($visitedUp).add(root)
|
|
166
|
+
|
|
154
167
|
const me = { entity, relative: element.parent, element }
|
|
155
168
|
if (prev) prev.next = me
|
|
156
169
|
if (element.parent['@PersonalData.EntitySemantics'] === 'DataSubject') {
|
|
@@ -159,14 +172,15 @@ const _getDataSubjectUp = (model, entity, prev, next, result) => {
|
|
|
159
172
|
return result
|
|
160
173
|
} else {
|
|
161
174
|
// dfs is a must here
|
|
162
|
-
result = _getDataSubjectUp(model, element.parent, me, next || me, result)
|
|
175
|
+
result = _getDataSubjectUp(root, model, element.parent, me, next || me, result)
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
return result
|
|
166
179
|
}
|
|
167
180
|
|
|
168
|
-
const _getDataSubjectDown = (entity, prev, next) => {
|
|
181
|
+
const _getDataSubjectDown = (root, entity, prev, next) => {
|
|
169
182
|
const associations = Object.values(entity.associations || {}).filter(e => !e._isBacklink)
|
|
183
|
+
// bfs makes more sense here -> check all own assocs first before going deeper
|
|
170
184
|
for (const element of associations) {
|
|
171
185
|
const me = { entity, relative: entity, element }
|
|
172
186
|
if (element._target['@PersonalData.EntitySemantics'] === 'DataSubject') {
|
|
@@ -174,23 +188,26 @@ const _getDataSubjectDown = (entity, prev, next) => {
|
|
|
174
188
|
return { dataSubjectEntity: element._target, subs: [next || me] }
|
|
175
189
|
}
|
|
176
190
|
}
|
|
177
|
-
// bfs makes more sense here
|
|
178
191
|
for (const element of associations) {
|
|
192
|
+
// cycle detection
|
|
193
|
+
if (!element.own($visitedDown)) element.set($visitedDown, new Set())
|
|
194
|
+
if (element.own($visitedDown).has(root)) continue
|
|
195
|
+
element.own($visitedDown).add(root)
|
|
196
|
+
|
|
179
197
|
const me = { entity, relative: entity, element }
|
|
180
198
|
if (prev) prev.next = me
|
|
181
|
-
const dataSubject = _getDataSubjectDown(element._target, me, next || me)
|
|
199
|
+
const dataSubject = _getDataSubjectDown(root, element._target, me, next || me)
|
|
182
200
|
if (dataSubject) return dataSubject
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
const getDataSubject = (entity, model) => {
|
|
187
|
-
|
|
188
|
-
if (entity.own(hash)) return entity[hash]
|
|
205
|
+
if (entity.own($dataSubject)) return entity[$dataSubject]
|
|
189
206
|
// entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
|
|
190
207
|
// be always below or always above 'DataSubject' entity in CSN tree
|
|
191
|
-
let dataSubjectInfo = _getDataSubjectUp(model, entity)
|
|
192
|
-
if (!dataSubjectInfo) dataSubjectInfo = _getDataSubjectDown(entity)
|
|
193
|
-
return entity.set(
|
|
208
|
+
let dataSubjectInfo = _getDataSubjectUp(entity.name, model, entity)
|
|
209
|
+
if (!dataSubjectInfo) dataSubjectInfo = _getDataSubjectDown(entity.name, entity)
|
|
210
|
+
return entity.set($dataSubject, dataSubjectInfo)
|
|
194
211
|
}
|
|
195
212
|
|
|
196
213
|
const _getDataSubjectsMap = req => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/audit-logging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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)",
|
package/srv/service.js
CHANGED
|
@@ -4,8 +4,6 @@ const Base = cds.outboxed ? cds.Service : require('@sap/cds/libx/_runtime/messag
|
|
|
4
4
|
|
|
5
5
|
module.exports = class AuditLogService extends Base {
|
|
6
6
|
async init() {
|
|
7
|
-
const outboxed = this.immediate instanceof cds.Service
|
|
8
|
-
|
|
9
7
|
// add common audit log entry fields
|
|
10
8
|
this.before('*', req => {
|
|
11
9
|
const { tenant, user, timestamp } = cds.context
|
|
@@ -22,8 +20,9 @@ module.exports = class AuditLogService extends Base {
|
|
|
22
20
|
this.log = this.emit
|
|
23
21
|
// NOTE: logSync is not a public API!
|
|
24
22
|
this.logSync = (...args) => {
|
|
25
|
-
if (
|
|
26
|
-
return this.send(...args)
|
|
23
|
+
if (cds.unboxed) return cds.unboxed(this).send(...args) //> cds >= 7.5
|
|
24
|
+
if (this.immediate instanceof cds.Service) return this.immediate.send(...args) //> cds ~ 7.4
|
|
25
|
+
return this.send(...args) //> cds <= 7.3
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
}
|