@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/README.md +7 -10
- package/cds-plugin.js +56 -44
- package/lib/_relation.js +103 -100
- package/lib/access.js +38 -37
- package/lib/modification.js +117 -108
- package/lib/utils.js +200 -157
- package/package.json +14 -11
- package/srv/log2als.js +5 -3
- package/srv/log2alsng.js +111 -105
- package/srv/log2console.js +7 -7
- package/srv/log2restv2.js +136 -109
- package/srv/service.js +17 -16
package/srv/log2restv2.js
CHANGED
|
@@ -1,126 +1,141 @@
|
|
|
1
|
-
const cds = require(
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
|
+
const { appMetadata } = require("../lib/utils");
|
|
2
3
|
|
|
3
|
-
const LOG = cds.log(
|
|
4
|
+
const LOG = cds.log("audit-log");
|
|
4
5
|
|
|
5
|
-
const AuditLogService = require(
|
|
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 =
|
|
14
|
-
this._auth =
|
|
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/) ?
|
|
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(
|
|
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 ===
|
|
27
|
-
return this._handle(data,
|
|
27
|
+
if (event === "SensitiveDataRead" || event.match(/^dataAccess/i)) {
|
|
28
|
+
return this._handle(data, "DATA_ACCESS");
|
|
28
29
|
}
|
|
29
|
-
if (event ===
|
|
30
|
-
data.success = true
|
|
31
|
-
return this._handle(data,
|
|
30
|
+
if (event === "PersonalDataModified" || event.match(/^dataModification/i)) {
|
|
31
|
+
data.success = true;
|
|
32
|
+
return this._handle(data, "DATA_MODIFICATION");
|
|
32
33
|
}
|
|
33
|
-
if (event ===
|
|
34
|
-
data.success = true
|
|
35
|
-
return this._handle(data,
|
|
34
|
+
if (event === "ConfigurationModified" || event.match(/^configChange/i)) {
|
|
35
|
+
data.success = true;
|
|
36
|
+
return this._handle(data, "CONFIGURATION_CHANGE");
|
|
36
37
|
}
|
|
37
|
-
if (event ===
|
|
38
|
-
if (typeof data.data ===
|
|
39
|
-
return this._handle(data,
|
|
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) +
|
|
55
|
-
const data = {
|
|
56
|
-
|
|
57
|
-
|
|
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[
|
|
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 ?
|
|
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(
|
|
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 = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
headers.
|
|
87
|
-
headers.
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 ===
|
|
97
|
-
headers.authorization =
|
|
98
|
-
data.tenant = data.tenant === this._provider ?
|
|
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, {
|
|
102
|
-
|
|
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(
|
|
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:
|
|
132
|
-
DATA_MODIFICATION:
|
|
133
|
-
CONFIGURATION_CHANGE:
|
|
134
|
-
SECURITY_EVENT:
|
|
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:
|
|
138
|
-
DATA_MODIFICATION:
|
|
139
|
-
CONFIGURATION_CHANGE:
|
|
140
|
-
SECURITY_EVENT:
|
|
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(
|
|
163
|
+
const https = require("https");
|
|
149
164
|
|
|
150
165
|
async function _post(url, data, options) {
|
|
151
|
-
options.method ??=
|
|
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(
|
|
156
|
-
res.on(
|
|
157
|
-
const { statusCode
|
|
158
|
-
let body = Buffer.concat(chunks).toString()
|
|
159
|
-
if (res.headers[
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 =
|
|
166
|
-
|
|
167
|
-
|
|
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(
|
|
174
|
-
req.write(typeof 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(
|
|
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(
|
|
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(
|
|
7
|
-
const { tenant, user, timestamp } = cds.context
|
|
8
|
-
req.data.uuid ??= cds.utils.uuid()
|
|
9
|
-
// allows to specify
|
|
10
|
-
|
|
11
|
-
req.data.
|
|
12
|
-
req.data.
|
|
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
|
+
};
|