@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/srv/log2restv2.js CHANGED
@@ -1,126 +1,141 @@
1
- const cds = require('@sap/cds')
1
+ const cds = require("@sap/cds");
2
+ const { appMetadata } = require("../lib/utils");
2
3
 
3
- const LOG = cds.log('audit-log')
4
+ const LOG = cds.log("audit-log");
4
5
 
5
- const AuditLogService = require('./service')
6
+ const AuditLogService = require("./service");
6
7
 
7
8
  module.exports = class AuditLog2RESTv2 extends AuditLogService {
8
9
  async init() {
9
10
  // credentials stuff
10
- const { credentials } = this.options
11
- if (!credentials) throw new Error('No or malformed credentials for "audit-log"')
11
+ const { credentials } = this.options;
12
+ if (!credentials) throw new Error('No or malformed credentials for "audit-log"');
12
13
  if (!credentials.uaa) {
13
- this._plan = 'standard'
14
- this._auth = 'Basic ' + Buffer.from(credentials.user + ':' + credentials.password).toString('base64')
14
+ this._plan = "standard";
15
+ this._auth =
16
+ "Basic " + Buffer.from(credentials.user + ":" + credentials.password).toString("base64");
15
17
  } else {
16
- this._plan = credentials.url.match(/6081/) ? 'premium' : 'oauth2'
17
- this._tokens = new Map()
18
- this._provider = credentials.uaa.tenantid
18
+ this._plan = credentials.url.match(/6081|\/premium/) ? "premium" : "oauth2";
19
+ this._tokens = new Map();
20
+ this._provider = credentials.uaa.tenantid;
19
21
  }
20
- this._vcap = process.env.VCAP_APPLICATION ? JSON.parse(process.env.VCAP_APPLICATION) : null
21
22
 
22
- this.on('*', function (req) {
23
- const { event, data } = req
23
+ this.on("*", function (req) {
24
+ const { event, data } = req;
24
25
 
25
26
  // event.match() is used to support the old event names
26
- if (event === 'SensitiveDataRead' || event.match(/^dataAccess/i)) {
27
- return this._handle(data, 'DATA_ACCESS')
27
+ if (event === "SensitiveDataRead" || event.match(/^dataAccess/i)) {
28
+ return this._handle(data, "DATA_ACCESS");
28
29
  }
29
- if (event === 'PersonalDataModified' || event.match(/^dataModification/i)) {
30
- data.success = true
31
- return this._handle(data, 'DATA_MODIFICATION')
30
+ if (event === "PersonalDataModified" || event.match(/^dataModification/i)) {
31
+ data.success = true;
32
+ return this._handle(data, "DATA_MODIFICATION");
32
33
  }
33
- if (event === 'ConfigurationModified' || event.match(/^configChange/i)) {
34
- data.success = true
35
- return this._handle(data, 'CONFIGURATION_CHANGE')
34
+ if (event === "ConfigurationModified" || event.match(/^configChange/i)) {
35
+ data.success = true;
36
+ return this._handle(data, "CONFIGURATION_CHANGE");
36
37
  }
37
- if (event === 'SecurityEvent' || event.match(/^security/i)) {
38
- if (typeof data.data === 'object') data.data = JSON.stringify(data.data)
39
- return this._handle(data, 'SECURITY_EVENT')
38
+ if (event === "SecurityEvent" || event.match(/^security/i)) {
39
+ if (typeof data.data === "object") data.data = JSON.stringify(data.data);
40
+ return this._handle(data, "SECURITY_EVENT");
40
41
  }
41
42
 
42
- LOG._warn && LOG.warn(`Event "${event}" is not implemented`)
43
- })
43
+ LOG._warn && LOG.warn(`Event "${event}" is not implemented`);
44
+ });
44
45
 
45
46
  // call AuditLogService's init
46
- await super.init()
47
+ await super.init();
47
48
  }
48
49
 
49
50
  async _getToken(tenant) {
50
- const { _tokens: tokens } = this
51
- if (tokens.has(tenant)) return tokens.get(tenant)
52
-
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
51
+ const { _tokens: tokens } = this;
52
+ if (tokens.has(tenant)) return tokens.get(tenant);
53
+
54
+ const { uaa } = this.options.credentials;
55
+ const url = (uaa.certurl || uaa.url) + "/oauth/token";
56
+ const data = {
57
+ grant_type: "client_credentials",
58
+ response_type: "token",
59
+ client_id: uaa.clientid
60
+ };
61
+ const options = {
62
+ headers: { "content-type": "application/x-www-form-urlencoded" }
63
+ };
64
+ if (tenant !== this._provider) options.headers["x-zid"] = tenant;
65
+
58
66
  // certificate or secret?
59
- if (uaa['credential-type'] === 'x509') {
60
- options.agent = new https.Agent({ cert: uaa.certificate, key: uaa.key })
67
+ if (uaa["credential-type"] === "x509") {
68
+ options.agent = new https.Agent({ cert: uaa.certificate, key: uaa.key });
61
69
  } else {
62
- data.client_secret = uaa.clientsecret
70
+ data.client_secret = uaa.clientsecret;
63
71
  }
64
72
  const urlencoded = Object.keys(data).reduce((acc, cur) => {
65
- acc += (acc ? '&' : '') + cur + '=' + data[cur]
66
- return acc
67
- }, '')
73
+ acc += (acc ? "&" : "") + cur + "=" + data[cur];
74
+ return acc;
75
+ }, "");
68
76
  try {
69
- const { access_token, expires_in } = await _post(url, urlencoded, options)
70
- tokens.set(tenant, access_token)
77
+ const { access_token, expires_in } = await _post(url, urlencoded, options);
78
+ tokens.set(tenant, access_token);
71
79
  // remove token from cache 60 seconds before it expires
72
- setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000)
73
- return access_token
80
+ setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000);
81
+ return access_token;
74
82
  } catch (err) {
75
- LOG._trace && LOG.trace('error during token fetch:', err)
83
+ LOG._trace && LOG.trace("error during token fetch:", err);
76
84
  // 401 could also mean x-zid is not valid
77
- if (String(err.response?.statusCode).match(/^4\d\d$/)) err.unrecoverable = true
78
- throw err
85
+ if (String(err.response?.statusCode).match(/^4\d\d$/)) err.unrecoverable = true;
86
+ throw err;
79
87
  }
80
88
  }
81
89
 
82
90
  async _send(data, path) {
83
- const headers = { 'content-type': 'application/json;charset=utf-8' }
84
- if (this._vcap) {
85
- headers.XS_AUDIT_ORG = this._vcap.organization_name
86
- headers.XS_AUDIT_SPACE = this._vcap.space_name
87
- headers.XS_AUDIT_APP = this._vcap.application_name
91
+ const headers = { "content-type": "application/json;charset=utf-8" };
92
+
93
+ if (appMetadata.appName) {
94
+ headers.XS_AUDIT_ORG = appMetadata.organization_name;
95
+ headers.XS_AUDIT_SPACE = appMetadata.space_name;
96
+ headers.XS_AUDIT_APP = appMetadata.appName;
88
97
  }
89
- let url
90
- if (this._plan === 'standard') {
91
- url = this.options.credentials.url + PATHS.STANDARD[path]
92
- headers.authorization = this._auth
98
+
99
+ let url;
100
+ if (this._plan === "standard") {
101
+ url = this.options.credentials.url + PATHS.STANDARD[path];
102
+ headers.authorization = this._auth;
93
103
  } 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
- if (data.tenant === '$PROVIDER') data.tenant = this._provider
97
- headers.authorization = 'Bearer ' + (await this._getToken(data.tenant))
98
- data.tenant = data.tenant === this._provider ? '$PROVIDER' : '$SUBSCRIBER'
104
+ url = this.options.credentials.url + PATHS.OAUTH2[path];
105
+ data.tenant ??= this._provider; //> if request has no tenant, stay in provider account
106
+ if (data.tenant === "$PROVIDER") data.tenant = this._provider;
107
+ headers.authorization = "Bearer " + (await this._getToken(data.tenant));
108
+ data.tenant = data.tenant === this._provider ? "$PROVIDER" : "$SUBSCRIBER";
99
109
  }
100
110
  if (LOG._debug) {
101
- const _headers = Object.assign({}, headers, { authorization: headers.authorization.split(' ')[0] + ' ***' })
102
- LOG.debug(`sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`, _headers)
111
+ const _headers = Object.assign({}, headers, {
112
+ authorization: headers.authorization.split(" ")[0] + " ***"
113
+ });
114
+ LOG.debug(
115
+ `sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`,
116
+ _headers
117
+ );
103
118
  }
104
119
  try {
105
- await _post(url, data, { headers })
120
+ await _post(url, data, { headers });
106
121
  } catch (err) {
107
- LOG._trace && LOG.trace('error during log send:', err)
122
+ LOG._trace && LOG.trace("error during log send:", err);
108
123
  // 429 (rate limit) is not unrecoverable
109
124
  if (String(err.response?.statusCode).match(/^4\d\d$/) && err.response?.statusCode !== 429)
110
- err.unrecoverable = true
111
- throw err
125
+ err.unrecoverable = true;
126
+ throw err;
112
127
  }
113
128
  }
114
129
 
115
130
  async _handle(logs, path) {
116
- if (!Array.isArray(logs)) logs = [logs]
131
+ if (!Array.isArray(logs)) logs = [logs];
117
132
 
118
133
  // write the logs
119
- const errors = []
120
- await Promise.all(logs.map(log => this._send(log, path).catch(err => errors.push(err))))
121
- if (errors.length) throw _getErrorToThrow(errors)
134
+ const errors = [];
135
+ await Promise.all(logs.map((log) => this._send(log, path).catch((err) => errors.push(err))));
136
+ if (errors.length) throw _getErrorToThrow(errors);
122
137
  }
123
- }
138
+ };
124
139
 
125
140
  /*
126
141
  * consts
@@ -128,58 +143,70 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
128
143
 
129
144
  const PATHS = {
130
145
  STANDARD: {
131
- DATA_ACCESS: '/audit-log/v2/data-accesses',
132
- DATA_MODIFICATION: '/audit-log/v2/data-modifications',
133
- CONFIGURATION_CHANGE: '/audit-log/v2/configuration-changes',
134
- SECURITY_EVENT: '/audit-log/v2/security-events'
146
+ DATA_ACCESS: "/audit-log/v2/data-accesses",
147
+ DATA_MODIFICATION: "/audit-log/v2/data-modifications",
148
+ CONFIGURATION_CHANGE: "/audit-log/v2/configuration-changes",
149
+ SECURITY_EVENT: "/audit-log/v2/security-events"
135
150
  },
136
151
  OAUTH2: {
137
- DATA_ACCESS: '/audit-log/oauth2/v2/data-accesses',
138
- DATA_MODIFICATION: '/audit-log/oauth2/v2/data-modifications',
139
- CONFIGURATION_CHANGE: '/audit-log/oauth2/v2/configuration-changes',
140
- SECURITY_EVENT: '/audit-log/oauth2/v2/security-events'
152
+ DATA_ACCESS: "/audit-log/oauth2/v2/data-accesses",
153
+ DATA_MODIFICATION: "/audit-log/oauth2/v2/data-modifications",
154
+ CONFIGURATION_CHANGE: "/audit-log/oauth2/v2/configuration-changes",
155
+ SECURITY_EVENT: "/audit-log/oauth2/v2/security-events"
141
156
  }
142
- }
157
+ };
143
158
 
144
159
  /*
145
160
  * utils
146
161
  */
147
162
 
148
- const https = require('https')
163
+ const https = require("https");
149
164
 
150
165
  async function _post(url, data, options) {
151
- options.method ??= 'POST'
166
+ options.method ??= "POST";
152
167
  return new Promise((resolve, reject) => {
153
- const req = https.request(url, options, res => {
154
- const chunks = []
155
- res.on('data', chunk => chunks.push(chunk))
156
- res.on('end', () => {
157
- const { statusCode, statusMessage } = res
158
- let body = Buffer.concat(chunks).toString()
159
- if (res.headers['content-type']?.match(/json/)) body = JSON.parse(body)
168
+ const req = https.request(url, options, (res) => {
169
+ const chunks = [];
170
+ res.on("data", (chunk) => chunks.push(chunk));
171
+ res.on("end", () => {
172
+ const { statusCode } = res;
173
+ let body = Buffer.concat(chunks).toString();
174
+ if (res.headers["content-type"]?.match(/json/)) body = JSON.parse(body);
160
175
  if (res.statusCode >= 400) {
161
- // prettier-ignore
162
- const err = new Error(`Request failed with${statusMessage ? `: ${statusCode} - ${statusMessage}` : ` status ${statusCode}`}`)
163
- err.request = { method: options.method, url, headers: options.headers, body: data }
176
+ const message =
177
+ body && typeof body === "object" && !Array.isArray(body)
178
+ ? Object.values(body).join(" - ")
179
+ : "";
180
+ LOG._trace && LOG.trace(`Request body of failed audit-log: `, data);
181
+ const err = new Error(
182
+ `Request failed with${message ? `: ${statusCode} - ${message}` : ` status ${statusCode}`}`
183
+ );
184
+ err.request = {
185
+ method: options.method,
186
+ url,
187
+ headers: options.headers,
188
+ body: data
189
+ };
164
190
  if (err.request.headers.authorization)
165
- err.request.headers.authorization = err.request.headers.authorization.split(' ')[0] + ' ***'
166
- err.response = { statusCode, statusMessage, headers: res.headers, body }
167
- reject(err)
191
+ err.request.headers.authorization =
192
+ err.request.headers.authorization.split(" ")[0] + " ***";
193
+ err.response = { statusCode, headers: res.headers, body };
194
+ reject(err);
168
195
  } else {
169
- resolve(body)
196
+ resolve(body);
170
197
  }
171
- })
172
- })
173
- req.on('error', reject)
174
- req.write(typeof data === 'object' ? JSON.stringify(data) : data)
175
- req.end()
176
- })
198
+ });
199
+ });
200
+ req.on("error", reject);
201
+ req.write(typeof data === "object" ? JSON.stringify(data) : data);
202
+ req.end();
203
+ });
177
204
  }
178
205
 
179
206
  function _getErrorToThrow(errors) {
180
- if (errors.length === 1) return errors[0]
181
- const error = new cds.error('MULTIPLE_ERRORS')
182
- error.details = errors
183
- if (errors.some(e => e.unrecoverable)) error.unrecoverable = true
184
- return error
207
+ if (errors.length === 1) return errors[0];
208
+ const error = new cds.error("MULTIPLE_ERRORS");
209
+ error.details = errors;
210
+ if (errors.some((e) => e.unrecoverable)) error.unrecoverable = true;
211
+ return error;
185
212
  }
package/srv/service.js CHANGED
@@ -1,27 +1,28 @@
1
- const cds = require('@sap/cds')
1
+ const cds = require("@sap/cds");
2
2
 
3
3
  module.exports = class AuditLogService extends cds.Service {
4
4
  async init() {
5
5
  // add common audit log entry fields
6
- this.before('*', req => {
7
- const { tenant, user, timestamp } = cds.context
8
- req.data.uuid ??= cds.utils.uuid()
9
- // allows to specify undefined tenant in order to log to provider in multi-tenant scenarios
10
- if (!('tenant' in req.data)) req.data.tenant = tenant
11
- req.data.user ??= user.id
12
- req.data.time ??= timestamp
13
- })
6
+ this.before("*", (req) => {
7
+ const { tenant, user, timestamp } = cds.context;
8
+ req.data.uuid ??= cds.utils.uuid();
9
+ // allows to specify null as tenant in order to log to provider in multi-tenant scenarios
10
+ // NOTE: tenant: null is not a public API!
11
+ if (!("tenant" in req.data)) req.data.tenant = tenant;
12
+ req.data.user ??= user.id;
13
+ req.data.time ??= timestamp;
14
+ });
14
15
 
15
16
  // call OutboxService's init
16
- await super.init()
17
+ await super.init();
17
18
 
18
19
  // add self-explanatory api (await audit.log/logSync(event, data))
19
- this.log = this.emit
20
+ this.log = this.emit;
20
21
  // NOTE: logSync is not a public API!
21
22
  this.logSync = (...args) => {
22
- if (cds.unboxed) return cds.unboxed(this).send(...args) //> cds >= 7.5
23
- if (this.immediate instanceof cds.Service) return this.immediate.send(...args) //> cds ~ 7.4
24
- return this.send(...args) //> cds <= 7.3
25
- }
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
26
+ };
26
27
  }
27
- }
28
+ };