@cap-js/audit-logging 0.3.1 → 0.4.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 +85 -0
- package/README.md +1 -1
- package/lib/access.js +6 -2
- package/lib/modification.js +2 -2
- package/lib/utils.js +39 -30
- package/package.json +7 -7
- package/srv/log2restv2.js +34 -24
- package/srv/service.js +9 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
|
+
|
|
7
|
+
## Version 0.4.0 - 2023-10-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Support for Premium plan of SAP Audit Log Service
|
|
12
|
+
- Support for XSUAA credential type `x509`
|
|
13
|
+
- Support for generic outbox
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Always use outbox (as configured in project)
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Avoid dangling `SELECT`s to resolve data subject IDs, which resulted in "Transaction already closed" errors
|
|
22
|
+
|
|
23
|
+
## Version 0.3.2 - 2023-10-11
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- If the request has no tenant (e.g., Unauthorized), the audit log shall be sent to the provider account
|
|
28
|
+
|
|
29
|
+
## Version 0.3.1 - 2023-09-25
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Defaulting of `@PersonalData.DataSubjectRole` to entity name
|
|
34
|
+
- Overriding service configuration
|
|
35
|
+
|
|
36
|
+
## Version 0.3.0 - 2023-09-05
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Default value for `cds.requires['audit-log'].handle` changed to `['READ', 'WRITE']`, i.e., accessing sensitive data is now logged by default.
|
|
41
|
+
|
|
42
|
+
## Version 0.2.0 - 2023-09-01
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- Export class `AuditLogService` for extending in custom implementations as follows:
|
|
47
|
+
```js
|
|
48
|
+
const { AuditLogService } = require('@cap-js/audit-logging')
|
|
49
|
+
class MyAuditLogService extends AuditLogService {
|
|
50
|
+
async init() {
|
|
51
|
+
[...]
|
|
52
|
+
// call AuditLogService's init
|
|
53
|
+
await super.init()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
module.exports = MyAuditLogService
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Version 0.1.0 - 2023-08-18
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- New API:
|
|
64
|
+
- `await audit.log('<event>', <data>)` for asynchronous logs (cf. `emit`)
|
|
65
|
+
- `await audit.logSync('<event>', <data>)` for synchronous logs (cf. `send`)
|
|
66
|
+
- New REST API-based schema with auto-filled `LogEntry` aspect
|
|
67
|
+
- New events `SensitiveDataRead`, `PersonalDataModified`, `ConfigurationModified`, and `SecurityEvent`
|
|
68
|
+
- Full support for OAuth2 plan of SAP Audit Log Service
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- Whether reading sensitive data and modifying personal data is logged is determined by `cds.requires['audit-log'].handle: [...]`.
|
|
73
|
+
Possible values in the array are `READ` and/ or `WRITE`, with `WRITE` as the sole default entry.
|
|
74
|
+
Hence, accessing sensitive data is not logged by default.
|
|
75
|
+
- Integration with SAP Audit Log Service via REST API instead of client library (`@sap/audit-logging`)
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- Various glitches in log calculation
|
|
80
|
+
|
|
81
|
+
### Removed
|
|
82
|
+
|
|
83
|
+
- Old events `dataAccessLog`, `dataModificationLog`, `configChangeLog`, and `securityLog`
|
|
84
|
+
- `@AuditLog.Operation` annotations are ignored. Having the plugin as dependency signals the intent to audit log.
|
|
85
|
+
- `cds.features.audit_personal_data: true` is no longer necessary. Instead, simply add the plugin as a dependency.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
## About this project
|
|
5
5
|
|
|
6
|
-
`@cap-js/audit-logging` is a CDS plugin providing integration to the SAP
|
|
6
|
+
`@cap-js/audit-logging` is a 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.
|
|
7
7
|
|
|
8
8
|
Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs/guides/data-privacy).
|
|
9
9
|
|
package/lib/access.js
CHANGED
|
@@ -11,7 +11,7 @@ const {
|
|
|
11
11
|
addObjectID,
|
|
12
12
|
addDataSubject,
|
|
13
13
|
addDataSubjectForDetailsEntity,
|
|
14
|
-
|
|
14
|
+
resolveDataSubjects
|
|
15
15
|
} = require('./utils')
|
|
16
16
|
|
|
17
17
|
let audit
|
|
@@ -59,7 +59,11 @@ const auditAccess = async function (data, req) {
|
|
|
59
59
|
const _data = Array.isArray(data) ? data : [data]
|
|
60
60
|
_data.forEach(row => templateProcessor({ processFn: _processorFnAccess(accessLogs, this.model, req), row, template }))
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
for (const each of Object.keys(accessLogs)) if (!accessLogs[each].attributes.length) delete accessLogs[each]
|
|
63
|
+
if (!Object.keys(accessLogs).length) return
|
|
64
|
+
|
|
65
|
+
await resolveDataSubjects(accessLogs, req)
|
|
66
|
+
const accesses = Object.values(accessLogs).filter(ele => ele.attributes.length)
|
|
63
67
|
if (!accesses.length) return
|
|
64
68
|
|
|
65
69
|
audit = audit || (await cds.connect.to('audit-log'))
|
package/lib/modification.js
CHANGED
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
addObjectID,
|
|
13
13
|
addDataSubject,
|
|
14
14
|
addDataSubjectForDetailsEntity,
|
|
15
|
-
|
|
15
|
+
resolveDataSubjects
|
|
16
16
|
} = require('./utils')
|
|
17
17
|
|
|
18
18
|
let audit
|
|
@@ -152,7 +152,7 @@ const _calcModificationLogsHandler = async function (req, beforeWrite, that) {
|
|
|
152
152
|
|
|
153
153
|
// execute the data subject promises before going along to on phase
|
|
154
154
|
// guarantees that the reads are executed before the data is modified
|
|
155
|
-
await
|
|
155
|
+
await resolveDataSubjects(modificationLogs, req)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
const calcModLogs4Before = function (req) {
|
package/lib/utils.js
CHANGED
|
@@ -5,12 +5,14 @@ const hasPersonalData = entity => {
|
|
|
5
5
|
// default role to entity name
|
|
6
6
|
if (entity['@PersonalData.EntitySemantics'] === 'DataSubject' && !entity['@PersonalData.DataSubjectRole'])
|
|
7
7
|
entity['@PersonalData.DataSubjectRole'] = entity.name.match(/\w+/g).pop()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
// prettier-ignore
|
|
9
|
+
const hasPersonalData = !!Object.values(entity.elements).some(element =>
|
|
10
|
+
element['@PersonalData.IsPotentiallyPersonal'] ||
|
|
11
|
+
element['@PersonalData.IsPotentiallySensitive'] ||
|
|
12
|
+
(element['@PersonalData.FieldSemantics'] && element['@PersonalData.FieldSemantics'] === 'DataSubjectID'))
|
|
13
|
+
// cache result
|
|
14
|
+
entity.own('_hasPersonalData', () => hasPersonalData)
|
|
15
|
+
return hasPersonalData
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const getMapKeyForCurrentRequest = req => {
|
|
@@ -118,22 +120,19 @@ const _buildSubSelect = (model, { entity, relative, element, next }, row, previo
|
|
|
118
120
|
return childCqn
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
const
|
|
123
|
+
const _getDataSubjectIdQuery = ({ dataSubjectEntity, subs }, row, model) => {
|
|
122
124
|
const keys = Object.values(dataSubjectEntity.keys)
|
|
123
125
|
const as = _alias(dataSubjectEntity)
|
|
124
126
|
|
|
125
|
-
const cqn = SELECT.
|
|
127
|
+
const cqn = SELECT.one
|
|
128
|
+
.from({ ref: [dataSubjectEntity.name], as })
|
|
126
129
|
.columns(_keyColumns(keys, as))
|
|
127
130
|
.where(['exists', _buildSubSelect(model, subs[0], row)])
|
|
128
131
|
|
|
129
132
|
// entity reused in different branches => must check all
|
|
130
133
|
for (let i = 1; i < subs.length; i++) cqn.or(['exists', _buildSubSelect(model, subs[i], row)])
|
|
131
134
|
|
|
132
|
-
return cqn
|
|
133
|
-
const id = {}
|
|
134
|
-
for (const k in res[0]) id[k] = res[0][k]
|
|
135
|
-
return id
|
|
136
|
-
})
|
|
135
|
+
return cqn
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
const _getUps = (entity, model) => {
|
|
@@ -194,6 +193,14 @@ const getDataSubject = (entity, model) => {
|
|
|
194
193
|
return entity.set(hash, dataSubjectInfo)
|
|
195
194
|
}
|
|
196
195
|
|
|
196
|
+
const _getDataSubjectsMap = req => {
|
|
197
|
+
const mapKey = getMapKeyForCurrentRequest(req)
|
|
198
|
+
const _audit = (req.context._audit ??= {})
|
|
199
|
+
if (!_audit.dataSubjects) _audit.dataSubjects = new Map()
|
|
200
|
+
if (!_audit.dataSubjects.has(mapKey)) _audit.dataSubjects.set(mapKey, new Map())
|
|
201
|
+
return _audit.dataSubjects.get(mapKey)
|
|
202
|
+
}
|
|
203
|
+
|
|
197
204
|
const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => {
|
|
198
205
|
const dataSubjectInfo = getDataSubject(entity, model)
|
|
199
206
|
const role = dataSubjectInfo.dataSubjectEntity['@PersonalData.DataSubjectRole']
|
|
@@ -203,24 +210,26 @@ const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => {
|
|
|
203
210
|
* for each req (cf. $batch with atomicity) and data subject role (e.g., customer vs supplier),
|
|
204
211
|
* store (in audit data structure at context) and reuse a single promise to look up the respective data subject
|
|
205
212
|
*/
|
|
206
|
-
const
|
|
207
|
-
const _audit = (req.context._audit ??= {})
|
|
208
|
-
if (!_audit.dataSubjects) _audit.dataSubjects = new Map()
|
|
209
|
-
if (!_audit.dataSubjects.has(mapKey)) _audit.dataSubjects.set(mapKey, new Map())
|
|
210
|
-
const map = _audit.dataSubjects.get(mapKey)
|
|
213
|
+
const map = _getDataSubjectsMap(req)
|
|
211
214
|
if (map.has(role)) log.data_subject.id = map.get(role)
|
|
212
215
|
// REVISIT by downward lookups row might already contain ID - some potential to optimize
|
|
213
|
-
else map.set(role,
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
216
|
+
else map.set(role, _getDataSubjectIdQuery(dataSubjectInfo, row, model))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const resolveDataSubjects = async (logs, req) => {
|
|
220
|
+
const map = _getDataSubjectsMap(req)
|
|
221
|
+
for (const each of Object.values(logs)) {
|
|
222
|
+
if (each.data_subject.id instanceof cds.ql.Query) {
|
|
223
|
+
const q = each.data_subject.id
|
|
224
|
+
if (map.has(q)) {
|
|
225
|
+
each.data_subject.id = map.get(q)
|
|
226
|
+
} else {
|
|
227
|
+
const res = await q
|
|
228
|
+
map.set(q, res)
|
|
229
|
+
each.data_subject.id = res
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
module.exports = {
|
|
@@ -232,5 +241,5 @@ module.exports = {
|
|
|
232
241
|
addObjectID,
|
|
233
242
|
addDataSubject,
|
|
234
243
|
addDataSubjectForDetailsEntity,
|
|
235
|
-
|
|
244
|
+
resolveDataSubjects
|
|
236
245
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/audit-logging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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)",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"main": "cds-plugin.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"lib",
|
|
12
|
-
"srv"
|
|
12
|
+
"srv",
|
|
13
|
+
"CHANGELOG.md"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"lint": "npx eslint .",
|
|
@@ -19,9 +20,9 @@
|
|
|
19
20
|
"@sap/cds": "^7"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
23
|
+
"@cap-js/audit-logging": "file:.",
|
|
22
24
|
"@cap-js/sqlite": "^1",
|
|
23
25
|
"axios": "^1",
|
|
24
|
-
"better-sqlite3": "8.5.2",
|
|
25
26
|
"eslint": "^8",
|
|
26
27
|
"express": "^4",
|
|
27
28
|
"jest": "^29"
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"READ",
|
|
34
35
|
"WRITE"
|
|
35
36
|
],
|
|
37
|
+
"outbox": true,
|
|
36
38
|
"[development]": {
|
|
37
39
|
"kind": "audit-log-to-console"
|
|
38
40
|
},
|
|
@@ -42,15 +44,13 @@
|
|
|
42
44
|
},
|
|
43
45
|
"kinds": {
|
|
44
46
|
"audit-log-to-console": {
|
|
45
|
-
"impl": "@cap-js/audit-logging/srv/log2console"
|
|
46
|
-
"outbox": false
|
|
47
|
+
"impl": "@cap-js/audit-logging/srv/log2console"
|
|
47
48
|
},
|
|
48
49
|
"audit-log-to-restv2": {
|
|
49
50
|
"impl": "@cap-js/audit-logging/srv/log2restv2",
|
|
50
51
|
"vcap": {
|
|
51
52
|
"label": "auditlog"
|
|
52
|
-
}
|
|
53
|
-
"outbox": true
|
|
53
|
+
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
package/srv/log2restv2.js
CHANGED
|
@@ -9,12 +9,13 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
|
|
|
9
9
|
// credentials stuff
|
|
10
10
|
const { credentials } = this.options
|
|
11
11
|
if (!credentials) throw new Error('No or malformed credentials for "audit-log"')
|
|
12
|
-
if (credentials.uaa) {
|
|
13
|
-
this.
|
|
14
|
-
this._tokens = new Map()
|
|
15
|
-
this._providerTenant = credentials.uaa.tenantid
|
|
16
|
-
} else {
|
|
12
|
+
if (!credentials.uaa) {
|
|
13
|
+
this._plan = 'standard'
|
|
17
14
|
this._auth = 'Basic ' + Buffer.from(credentials.user + ':' + credentials.password).toString('base64')
|
|
15
|
+
} else {
|
|
16
|
+
this._plan = credentials.url.match(/6081/) ? 'premium' : 'oauth2'
|
|
17
|
+
this._tokens = new Map()
|
|
18
|
+
this._provider = credentials.uaa.tenantid
|
|
18
19
|
}
|
|
19
20
|
this._vcap = process.env.VCAP_APPLICATION ? JSON.parse(process.env.VCAP_APPLICATION) : null
|
|
20
21
|
|
|
@@ -49,26 +50,29 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
|
|
|
49
50
|
const { _tokens: tokens } = this
|
|
50
51
|
if (tokens.has(tenant)) return tokens.get(tenant)
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
const { uaa } = this.options.credentials
|
|
54
|
+
const url = (uaa.certurl || uaa.url) + '/oauth/token'
|
|
55
|
+
const data = { grant_type: 'client_credentials', response_type: 'token', client_id: uaa.clientid }
|
|
56
|
+
const options = { headers: { 'content-type': 'application/x-www-form-urlencoded' } }
|
|
57
|
+
if (tenant !== this._provider) options.headers['x-zid'] = tenant
|
|
58
|
+
// certificate or secret?
|
|
59
|
+
if (uaa['credential-type'] === 'x509') {
|
|
60
|
+
options.agent = new https.Agent({ cert: uaa.certificate, key: uaa.key })
|
|
61
|
+
} else {
|
|
62
|
+
data.client_secret = uaa.clientsecret
|
|
58
63
|
}
|
|
59
64
|
const urlencoded = Object.keys(data).reduce((acc, cur) => {
|
|
60
65
|
acc += (acc ? '&' : '') + cur + '=' + data[cur]
|
|
61
66
|
return acc
|
|
62
67
|
}, '')
|
|
63
|
-
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
|
|
64
|
-
if (tenant !== this._providerTenant) headers['x-zid'] = tenant
|
|
65
68
|
try {
|
|
66
|
-
const { access_token, expires_in } = await _post(url, urlencoded,
|
|
69
|
+
const { access_token, expires_in } = await _post(url, urlencoded, options)
|
|
67
70
|
tokens.set(tenant, access_token)
|
|
68
71
|
// remove token from cache 60 seconds before it expires
|
|
69
72
|
setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000)
|
|
70
73
|
return access_token
|
|
71
74
|
} catch (err) {
|
|
75
|
+
LOG._trace && LOG.trace('error during token fetch:', err)
|
|
72
76
|
// 401 could also mean x-zid is not valid
|
|
73
77
|
if (String(err.response?.statusCode).match(/^4\d\d$/)) err.unrecoverable = true
|
|
74
78
|
throw err
|
|
@@ -76,25 +80,30 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
async _send(data, path) {
|
|
79
|
-
let url
|
|
80
83
|
const headers = { 'content-type': 'application/json' }
|
|
81
|
-
// TODO: what are these for?
|
|
82
84
|
if (this._vcap) {
|
|
83
85
|
headers.XS_AUDIT_ORG = this._vcap.organization_name
|
|
84
86
|
headers.XS_AUDIT_SPACE = this._vcap.space_name
|
|
85
87
|
headers.XS_AUDIT_APP = this._vcap.application_name
|
|
86
88
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
headers.authorization = 'Bearer ' + (await this._getToken(data.tenant))
|
|
90
|
-
data.tenant = data.tenant === this._providerTenant ? '$PROVIDER' : '$SUBSCRIBER'
|
|
91
|
-
} else {
|
|
89
|
+
let url
|
|
90
|
+
if (this._plan === 'standard') {
|
|
92
91
|
url = this.options.credentials.url + PATHS.STANDARD[path]
|
|
93
92
|
headers.authorization = this._auth
|
|
93
|
+
} 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
|
+
headers.authorization = 'Bearer ' + (await this._getToken(data.tenant))
|
|
97
|
+
data.tenant = data.tenant === this._provider ? '$PROVIDER' : '$SUBSCRIBER'
|
|
98
|
+
}
|
|
99
|
+
if (LOG._debug) {
|
|
100
|
+
const _headers = Object.assign({}, headers, { authorization: headers.authorization.split(' ')[0] + ' ***' })
|
|
101
|
+
LOG.debug(`sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`, _headers)
|
|
94
102
|
}
|
|
95
103
|
try {
|
|
96
|
-
await _post(url, data, headers)
|
|
104
|
+
await _post(url, data, { headers })
|
|
97
105
|
} catch (err) {
|
|
106
|
+
LOG._trace && LOG.trace('error during log send:', err)
|
|
98
107
|
// 429 (rate limit) is not unrecoverable
|
|
99
108
|
if (String(err.response?.statusCode).match(/^4\d\d$/) && err.response?.statusCode !== 429)
|
|
100
109
|
err.unrecoverable = true
|
|
@@ -137,9 +146,10 @@ const PATHS = {
|
|
|
137
146
|
|
|
138
147
|
const https = require('https')
|
|
139
148
|
|
|
140
|
-
async function _post(url, data,
|
|
149
|
+
async function _post(url, data, options) {
|
|
150
|
+
options.method ??= 'POST'
|
|
141
151
|
return new Promise((resolve, reject) => {
|
|
142
|
-
const req = https.request(url,
|
|
152
|
+
const req = https.request(url, options, res => {
|
|
143
153
|
const chunks = []
|
|
144
154
|
res.on('data', chunk => chunks.push(chunk))
|
|
145
155
|
res.on('end', () => {
|
package/srv/service.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const OutboxService = require('@sap/cds/libx/_runtime/messaging/Outbox')
|
|
3
|
+
const Base = cds.outboxed ? cds.Service : require('@sap/cds/libx/_runtime/messaging/Outbox')
|
|
5
4
|
|
|
6
|
-
module.exports = class AuditLogService extends
|
|
5
|
+
module.exports = class AuditLogService extends Base {
|
|
7
6
|
async init() {
|
|
7
|
+
const outboxed = this.immediate instanceof cds.Service
|
|
8
|
+
|
|
8
9
|
// add common audit log entry fields
|
|
9
10
|
this.before('*', req => {
|
|
10
11
|
const { tenant, user, timestamp: time } = cds.context
|
|
@@ -16,6 +17,10 @@ module.exports = class AuditLogService extends OutboxService {
|
|
|
16
17
|
|
|
17
18
|
// add self-explanatory api (await audit.log/logSync(event, data))
|
|
18
19
|
this.log = this.emit
|
|
19
|
-
|
|
20
|
+
// NOTE: logSync is not a public API!
|
|
21
|
+
this.logSync = (...args) => {
|
|
22
|
+
if (outboxed) return this.immediate.send(...args)
|
|
23
|
+
return this.send(...args)
|
|
24
|
+
}
|
|
20
25
|
}
|
|
21
26
|
}
|