@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/README.md
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
`@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.
|
|
6
6
|
|
|
7
|
-
Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs/guides/data-privacy).
|
|
7
|
+
Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs/guides/data-privacy).
|
|
8
8
|
|
|
9
9
|
> [!IMPORTANT]
|
|
10
|
-
> The information in this file is by no means complete but enables you to get started quickly. Make sure to read the provided documentation at [cap.cloud.sap](https://cap.cloud.sap/docs/guides/data-privacy) to get the full picture.
|
|
11
|
-
|
|
10
|
+
> The information in this file is by no means complete but enables you to get started quickly. Make sure to read the provided documentation at [cap.cloud.sap](https://cap.cloud.sap/docs/guides/data-privacy) to get the full picture.
|
|
12
11
|
|
|
13
12
|
## Preliminaries
|
|
14
13
|
|
|
@@ -20,7 +19,6 @@ cd incidents-app
|
|
|
20
19
|
npm i
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
|
|
24
22
|
## Setup
|
|
25
23
|
|
|
26
24
|
To enable audit logging, simply add this self-configuring plugin package to your project:
|
|
@@ -29,7 +27,6 @@ To enable audit logging, simply add this self-configuring plugin package to your
|
|
|
29
27
|
npm add @cap-js/audit-logging
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
|
|
33
30
|
## Annotate Personal Data
|
|
34
31
|
|
|
35
32
|
Identify entities and elements (potentially) holding personal data using `@PersonalData` annotations. Create a `db/data-privacy.cds` file and add the following:
|
|
@@ -65,24 +62,27 @@ annotate my.Incidents with @PersonalData : {
|
|
|
65
62
|
};
|
|
66
63
|
|
|
67
64
|
```
|
|
65
|
+
|
|
68
66
|
Learn more about the annotations in capire:
|
|
67
|
+
|
|
69
68
|
- [@PersonalData.EntitySemantics](https://cap.cloud.sap/docs/guides/data-privacy/annotations#entitysemantics)
|
|
70
69
|
- [@PersonalData.EntitySemantics: 'DataSubject'](https://cap.cloud.sap/docs/guides/data-privacy/annotations#datasubjectrole)
|
|
71
70
|
- [@PersonalData.FieldSemantics: 'DataSubjectID'](https://cap.cloud.sap/docs/guides/data-privacy/annotations#fieldsemantics-datasubjectid)
|
|
72
71
|
- [@PersonalData.IsPotentiallyPersonal](https://cap.cloud.sap/docs/guides/data-privacy/annotations#ispotentiallypersonal)
|
|
73
72
|
- [@PersonalData.IsPotentiallySensitive](https://cap.cloud.sap/docs/guides/data-privacy/annotations#ispotentiallysensitive)
|
|
74
73
|
|
|
75
|
-
|
|
76
74
|
## Test-Drive Locally
|
|
77
75
|
|
|
78
76
|
You've prepared everything to log personal data-related events. Let's see that in action.
|
|
79
77
|
|
|
80
78
|
Start the server as usual:
|
|
79
|
+
|
|
81
80
|
```sh
|
|
82
81
|
cds watch
|
|
83
82
|
```
|
|
84
83
|
|
|
85
84
|
Send an update request that changes personal data:
|
|
85
|
+
|
|
86
86
|
```http
|
|
87
87
|
PATCH http://localhost:4004/odata/v4/admin/Customers('1004155')
|
|
88
88
|
Authorization: Basic alice:in-wonderland
|
|
@@ -95,6 +95,7 @@ Content-Type: application/json
|
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
See the audit logs in the server's console output:
|
|
98
|
+
|
|
98
99
|
```sh
|
|
99
100
|
[audit-log] - PersonalDataModified: {
|
|
100
101
|
data_subject: {
|
|
@@ -117,7 +118,6 @@ See the audit logs in the server's console output:
|
|
|
117
118
|
}
|
|
118
119
|
```
|
|
119
120
|
|
|
120
|
-
|
|
121
121
|
## In Production
|
|
122
122
|
|
|
123
123
|
The end-to-end out-of-the-box functionality provided by this plugin requires a paid-for instance of the [SAP Audit Log service for customers](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-write-api-for-customers?locale=en-US). However, it is possible to provide an own implementation that writes the audit logs to a custom store.
|
|
@@ -126,17 +126,14 @@ The end-to-end out-of-the-box functionality provided by this plugin requires a p
|
|
|
126
126
|
|
|
127
127
|
[_Learn more about custom audit logging._](https://cap.cloud.sap/docs/guides/data-privacy/audit-logging#custom-audit-logging)
|
|
128
128
|
|
|
129
|
-
|
|
130
129
|
## Support, Feedback, Contributing
|
|
131
130
|
|
|
132
131
|
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/audit-logging/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
|
|
133
132
|
|
|
134
|
-
|
|
135
133
|
## Code of Conduct
|
|
136
134
|
|
|
137
135
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.
|
|
138
136
|
|
|
139
|
-
|
|
140
137
|
## Licensing
|
|
141
138
|
|
|
142
139
|
Copyright 2023 SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js/audit-logging).
|
package/cds-plugin.js
CHANGED
|
@@ -5,7 +5,7 @@ const {
|
|
|
5
5
|
addDiffToCtx,
|
|
6
6
|
calcModLogs4Before,
|
|
7
7
|
calcModLogs4After,
|
|
8
|
-
emitModLogs
|
|
8
|
+
emitModLogs
|
|
9
9
|
} = require("./lib/modification");
|
|
10
10
|
const { hasPersonalData } = require("./lib/utils");
|
|
11
11
|
|
|
@@ -30,13 +30,9 @@ cds.on("served", (services) => {
|
|
|
30
30
|
if (entity["@PersonalData.EntitySemantics"] !== "DataSubject") continue;
|
|
31
31
|
for (const e of service.entities) {
|
|
32
32
|
for (const k in e.associations) {
|
|
33
|
-
if (
|
|
34
|
-
e.associations[k].target === entity.name &&
|
|
35
|
-
k !== "SiblingEntity"
|
|
36
|
-
) {
|
|
33
|
+
if (e.associations[k].target === entity.name && k !== "SiblingEntity") {
|
|
37
34
|
e["@PersonalData.EntitySemantics"] ??= "Other";
|
|
38
|
-
e.associations[k]["@PersonalData.FieldSemantics"] ??=
|
|
39
|
-
"DataSubjectID";
|
|
35
|
+
e.associations[k]["@PersonalData.FieldSemantics"] ??= "DataSubjectID";
|
|
40
36
|
if (!relevantEntities.includes(e)) relevantEntities.push(e);
|
|
41
37
|
}
|
|
42
38
|
}
|
|
@@ -47,18 +43,18 @@ cds.on("served", (services) => {
|
|
|
47
43
|
if (!(service instanceof cds.ApplicationService)) continue;
|
|
48
44
|
service.after("READ", async (res, req) => {
|
|
49
45
|
// Checking for req.target._service to make sure only entities within services are considered
|
|
50
|
-
if (!req.target
|
|
46
|
+
if (!req.target?._service || !hasPersonalData(req.target)) return;
|
|
51
47
|
await auditAccess.call(service, res, req);
|
|
52
48
|
});
|
|
53
49
|
|
|
54
50
|
service.after(WRITE, async (res, req) => {
|
|
55
|
-
if (!req.target
|
|
51
|
+
if (!req.target?._service || !hasPersonalData(req.target)) return;
|
|
56
52
|
await emitModLogs.call(service, res, req);
|
|
57
53
|
});
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
db.before("CREATE", async (req) => {
|
|
61
|
-
if (!req.target
|
|
57
|
+
if (!req.target?._service || !hasPersonalData(req.target)) return;
|
|
62
58
|
await addDiffToCtx.call(db, req);
|
|
63
59
|
});
|
|
64
60
|
/*
|
|
@@ -67,11 +63,11 @@ cds.on("served", (services) => {
|
|
|
67
63
|
* deep updates can contain new, modified and deleted data -> both phases
|
|
68
64
|
*/
|
|
69
65
|
db.after(["CREATE", "UPDATE"], async (res, req) => {
|
|
70
|
-
if (!req.target
|
|
66
|
+
if (!req.target?._service || !hasPersonalData(req.target)) return;
|
|
71
67
|
await calcModLogs4After.call(db, res, req);
|
|
72
68
|
});
|
|
73
69
|
db.before(["DELETE", "UPDATE"], async (req) => {
|
|
74
|
-
if (!req.target
|
|
70
|
+
if (!req.target?._service || !hasPersonalData(req.target)) return;
|
|
75
71
|
await addDiffToCtx.call(db, req);
|
|
76
72
|
await calcModLogs4Before.call(db, req);
|
|
77
73
|
});
|
|
@@ -81,5 +77,5 @@ cds.on("served", (services) => {
|
|
|
81
77
|
* Export base class for extending in custom implementations
|
|
82
78
|
*/
|
|
83
79
|
module.exports = {
|
|
84
|
-
AuditLogService: require("./srv/service")
|
|
80
|
+
AuditLogService: require("./srv/service")
|
|
85
81
|
};
|
package/lib/_relation.js
CHANGED
|
@@ -1,208 +1,211 @@
|
|
|
1
|
-
let initializing = false
|
|
1
|
+
let initializing = false;
|
|
2
2
|
|
|
3
3
|
class Relation {
|
|
4
4
|
constructor(csn, path = []) {
|
|
5
|
-
if (!initializing) throw new Error(`Do not new a relation, use 'Relation.to()' instead`)
|
|
6
|
-
Object.defineProperty(this,
|
|
7
|
-
Object.defineProperty(this,
|
|
5
|
+
if (!initializing) throw new Error(`Do not new a relation, use 'Relation.to()' instead`);
|
|
6
|
+
Object.defineProperty(this, "csn", { get: () => csn });
|
|
7
|
+
Object.defineProperty(this, "path", {
|
|
8
8
|
get: () => path,
|
|
9
|
-
set: _ => {
|
|
10
|
-
path = _
|
|
9
|
+
set: (_) => {
|
|
10
|
+
path = _;
|
|
11
11
|
}
|
|
12
|
-
})
|
|
13
|
-
if (csn.target) Object.defineProperty(this,
|
|
14
|
-
initializing = false
|
|
12
|
+
});
|
|
13
|
+
if (csn.target) Object.defineProperty(this, "target", { get: () => csn.target });
|
|
14
|
+
initializing = false;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
static to(from, name) {
|
|
18
|
-
initializing = true
|
|
19
|
-
if (!name) return new Relation(from)
|
|
20
|
-
return from._elements[name] && new Relation(from._elements[name], [...from.path, name])
|
|
18
|
+
initializing = true;
|
|
19
|
+
if (!name) return new Relation(from);
|
|
20
|
+
return from._elements[name] && new Relation(from._elements[name], [...from.path, name]);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
_has(prop) {
|
|
24
|
-
return Reflect.has(this, prop)
|
|
24
|
+
return Reflect.has(this, prop);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
get _elements() {
|
|
28
|
-
if (this.csn.elements) return this.csn.elements
|
|
29
|
-
if (this.csn._target && this.csn._target.elements) return this.csn._target.elements
|
|
28
|
+
if (this.csn.elements) return this.csn.elements;
|
|
29
|
+
if (this.csn._target && this.csn._target.elements) return this.csn._target.elements;
|
|
30
30
|
// if (csn.targetAspect) relation.elements = model.definitions[csn.targetAspect].elements
|
|
31
31
|
// if (csn.kind = 'type') relation.elements = model.definitions[csn.type].element
|
|
32
|
-
return {}
|
|
32
|
+
return {};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
join(fromAlias =
|
|
36
|
-
return _getOnCond(this.csn, this.path, { select: fromAlias, join: toAlias })
|
|
35
|
+
join(fromAlias = "", toAlias = "") {
|
|
36
|
+
return _getOnCond(this.csn, this.path, { select: fromAlias, join: toAlias });
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const exposeRelation = relation => Object.defineProperty({},
|
|
40
|
+
const exposeRelation = (relation) => Object.defineProperty({}, "_", { get: () => relation });
|
|
41
41
|
|
|
42
|
-
const relationHandler = relation => ({
|
|
42
|
+
const relationHandler = (relation) => ({
|
|
43
43
|
get: (target, name) => {
|
|
44
|
-
const path = name.split(
|
|
45
|
-
const prop = path.join(
|
|
44
|
+
const path = name.split(",");
|
|
45
|
+
const prop = path.join("_");
|
|
46
46
|
if (!target[prop]) {
|
|
47
47
|
if (path.length === 1) {
|
|
48
48
|
// REVISIT: property 'join' must not be used in CSN to make this working
|
|
49
|
-
if (relation._has(prop)) return relation[prop]
|
|
50
|
-
const newRelation = Relation.to(relation, prop)
|
|
49
|
+
if (relation._has(prop)) return relation[prop];
|
|
50
|
+
const newRelation = Relation.to(relation, prop);
|
|
51
51
|
if (newRelation) {
|
|
52
|
-
target[prop] = new Proxy(exposeRelation(newRelation), relationHandler(newRelation))
|
|
52
|
+
target[prop] = new Proxy(exposeRelation(newRelation), relationHandler(newRelation));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
return target[prop]
|
|
55
|
+
return target[prop];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
target[prop] = path.reduce(
|
|
59
|
-
|
|
58
|
+
target[prop] = path.reduce(
|
|
59
|
+
(relation, value) => relation[value] || relation.csn._relations[value],
|
|
60
|
+
relation
|
|
61
|
+
);
|
|
62
|
+
target[prop].path = path;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
return target[prop]
|
|
65
|
+
return target[prop];
|
|
63
66
|
}
|
|
64
|
-
})
|
|
67
|
+
});
|
|
65
68
|
|
|
66
69
|
module.exports = {
|
|
67
70
|
Relation,
|
|
68
71
|
exposeRelation,
|
|
69
72
|
relationHandler
|
|
70
|
-
}
|
|
73
|
+
};
|
|
71
74
|
|
|
72
75
|
//
|
|
73
76
|
// ----- utils
|
|
74
77
|
//
|
|
75
78
|
|
|
76
|
-
const _prefixForStruct = element => {
|
|
77
|
-
const prefixes = []
|
|
78
|
-
let parent = element.parent
|
|
79
|
-
while (parent && parent.kind !==
|
|
80
|
-
prefixes.push(parent.name)
|
|
81
|
-
parent = parent.parent
|
|
79
|
+
const _prefixForStruct = (element) => {
|
|
80
|
+
const prefixes = [];
|
|
81
|
+
let parent = element.parent;
|
|
82
|
+
while (parent && parent.kind !== "entity") {
|
|
83
|
+
prefixes.push(parent.name);
|
|
84
|
+
parent = parent.parent;
|
|
82
85
|
}
|
|
83
|
-
return prefixes.length ? prefixes.reverse().join(
|
|
84
|
-
}
|
|
86
|
+
return prefixes.length ? prefixes.reverse().join("_") + "_" : "";
|
|
87
|
+
};
|
|
85
88
|
|
|
86
89
|
const _toRef = (alias, column) => {
|
|
87
|
-
if (Array.isArray(column)) column = column.join(
|
|
88
|
-
return { ref: alias ? [alias, column] : [column] }
|
|
89
|
-
}
|
|
90
|
+
if (Array.isArray(column)) column = column.join("_");
|
|
91
|
+
return { ref: alias ? [alias, column] : [column] };
|
|
92
|
+
};
|
|
90
93
|
|
|
91
94
|
const _adaptRefs = (onCond, path, { select, join }) => {
|
|
92
|
-
const _adaptEl = el => {
|
|
93
|
-
const ref = el.ref
|
|
95
|
+
const _adaptEl = (el) => {
|
|
96
|
+
const ref = el.ref;
|
|
94
97
|
|
|
95
98
|
if (ref) {
|
|
96
|
-
if (ref[0] === path.join(
|
|
97
|
-
return _toRef(select, ref.slice(1))
|
|
99
|
+
if (ref[0] === path.join("_") && ref[1]) {
|
|
100
|
+
return _toRef(select, ref.slice(1));
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
// no alias for special $user of canonical localized association
|
|
101
|
-
if (ref[0] ===
|
|
102
|
-
return _toRef(undefined, ref.slice(0))
|
|
104
|
+
if (ref[0] === "$user" && path[0] === "localized") {
|
|
105
|
+
return _toRef(undefined, ref.slice(0));
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
return _toRef(join, ref.slice(0))
|
|
108
|
+
return _toRef(join, ref.slice(0));
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
if (el.xpr) return { xpr: el.xpr.map(_adaptEl) }
|
|
109
|
-
return el
|
|
110
|
-
}
|
|
111
|
+
if (el.xpr) return { xpr: el.xpr.map(_adaptEl) };
|
|
112
|
+
return el;
|
|
113
|
+
};
|
|
111
114
|
|
|
112
|
-
return onCond.map(_adaptEl)
|
|
113
|
-
}
|
|
115
|
+
return onCond.map(_adaptEl);
|
|
116
|
+
};
|
|
114
117
|
|
|
115
118
|
const _replace$selfAndAliasOnCond = (xpr, csnElement, aliases, path) => {
|
|
116
|
-
const selfIndex = xpr.findIndex(({ ref }) => ref?.[0] ===
|
|
119
|
+
const selfIndex = xpr.findIndex(({ ref }) => ref?.[0] === "$self");
|
|
117
120
|
if (selfIndex != -1) {
|
|
118
|
-
let backLinkIndex
|
|
119
|
-
if (xpr[selfIndex + 1] && xpr[selfIndex + 1] ===
|
|
120
|
-
if (xpr[selfIndex - 1] && xpr[selfIndex - 1] ===
|
|
121
|
+
let backLinkIndex;
|
|
122
|
+
if (xpr[selfIndex + 1] && xpr[selfIndex + 1] === "=") backLinkIndex = selfIndex + 2;
|
|
123
|
+
if (xpr[selfIndex - 1] && xpr[selfIndex - 1] === "=") backLinkIndex = selfIndex - 2;
|
|
121
124
|
if (backLinkIndex != null) {
|
|
122
|
-
const ref = xpr[backLinkIndex].ref
|
|
123
|
-
const backlinkName = ref[ref.length - 1]
|
|
125
|
+
const ref = xpr[backLinkIndex].ref;
|
|
126
|
+
const backlinkName = ref[ref.length - 1];
|
|
124
127
|
const mutOnCond = _newOnConditions(csnElement._backlink, [backlinkName], {
|
|
125
128
|
select: aliases.join,
|
|
126
129
|
join: aliases.select
|
|
127
|
-
})
|
|
130
|
+
});
|
|
128
131
|
|
|
129
|
-
xpr.splice(Math.min(backLinkIndex, selfIndex), 3, ...mutOnCond)
|
|
132
|
+
xpr.splice(Math.min(backLinkIndex, selfIndex), 3, ...mutOnCond);
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
for (let i = 0; i < xpr.length; i++) {
|
|
134
|
-
const element = xpr[i]
|
|
137
|
+
const element = xpr[i];
|
|
135
138
|
if (element.xpr) {
|
|
136
|
-
_replace$selfAndAliasOnCond(element.xpr, csnElement, aliases, path)
|
|
137
|
-
continue
|
|
139
|
+
_replace$selfAndAliasOnCond(element.xpr, csnElement, aliases, path);
|
|
140
|
+
continue;
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
if (element.ref) {
|
|
141
|
-
if (element.ref[0] === path.join(
|
|
142
|
-
element.ref = _toRef(aliases.select, element.ref.slice(1)).ref
|
|
143
|
-
continue
|
|
144
|
+
if (element.ref[0] === path.join("_") && element.ref[1]) {
|
|
145
|
+
element.ref = _toRef(aliases.select, element.ref.slice(1)).ref;
|
|
146
|
+
continue;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
// no alias for special $user of canonical localized association
|
|
147
|
-
if (element.ref[0] ===
|
|
148
|
-
element.ref = _toRef(undefined, element.ref.slice(0)).ref
|
|
149
|
-
continue
|
|
150
|
+
if (element.ref[0] === "$user" && path[0] === "localized") {
|
|
151
|
+
element.ref = _toRef(undefined, element.ref.slice(0)).ref;
|
|
152
|
+
continue;
|
|
150
153
|
}
|
|
151
154
|
//no alias for special $now variable
|
|
152
|
-
if (element.ref[0] ===
|
|
153
|
-
continue
|
|
155
|
+
if (element.ref[0] === "$now") {
|
|
156
|
+
continue;
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
if (element.ref[0] === aliases.join || element.ref[0] === aliases.select) {
|
|
157
160
|
// nothing todo here, as already right alias
|
|
158
|
-
continue
|
|
161
|
+
continue;
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
element.ref = _toRef(aliases.join, element.ref.slice(0)).ref
|
|
164
|
+
element.ref = _toRef(aliases.join, element.ref.slice(0)).ref;
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
|
-
}
|
|
167
|
+
};
|
|
165
168
|
|
|
166
169
|
const _args = (csnElement, path, aliases) => {
|
|
167
|
-
const onCond = csnElement.on
|
|
168
|
-
if (!onCond || onCond.length === 0) return []
|
|
169
|
-
if (onCond.length < 3 && !onCond[0]?.xpr) return onCond
|
|
170
|
-
if (!csnElement._isSelfManaged) return _adaptRefs(onCond, path, aliases)
|
|
170
|
+
const onCond = csnElement.on;
|
|
171
|
+
if (!onCond || onCond.length === 0) return [];
|
|
172
|
+
if (onCond.length < 3 && !onCond[0]?.xpr) return onCond;
|
|
173
|
+
if (!csnElement._isSelfManaged) return _adaptRefs(onCond, path, aliases);
|
|
171
174
|
|
|
172
|
-
const onCondCopy = JSON.parse(JSON.stringify(onCond))
|
|
173
|
-
_replace$selfAndAliasOnCond(onCondCopy, csnElement, aliases, path)
|
|
175
|
+
const onCondCopy = JSON.parse(JSON.stringify(onCond));
|
|
176
|
+
_replace$selfAndAliasOnCond(onCondCopy, csnElement, aliases, path);
|
|
174
177
|
|
|
175
|
-
return onCondCopy
|
|
176
|
-
}
|
|
178
|
+
return onCondCopy;
|
|
179
|
+
};
|
|
177
180
|
|
|
178
181
|
// this is only for 2one managed w/o on-conditions, i.e. no static values are possible
|
|
179
182
|
const _foreignToOn = (csnElement, path, { select, join }) => {
|
|
180
|
-
const on = []
|
|
183
|
+
const on = [];
|
|
181
184
|
|
|
182
185
|
for (const key of csnElement._foreignKeys) {
|
|
183
186
|
if (on.length !== 0) {
|
|
184
|
-
on.push(
|
|
187
|
+
on.push("and");
|
|
185
188
|
}
|
|
186
189
|
|
|
187
|
-
const prefixChild = _prefixForStruct(key.childElement)
|
|
188
|
-
const ref1 = _toRef(select, prefixChild + key.childElement.name)
|
|
189
|
-
const structPrefix = path.length > 1 ? path.slice(0, -1) : []
|
|
190
|
-
const ref2 = _toRef(join, [...structPrefix, key.parentElement.name])
|
|
191
|
-
on.push(ref1,
|
|
190
|
+
const prefixChild = _prefixForStruct(key.childElement);
|
|
191
|
+
const ref1 = _toRef(select, prefixChild + key.childElement.name);
|
|
192
|
+
const structPrefix = path.length > 1 ? path.slice(0, -1) : [];
|
|
193
|
+
const ref2 = _toRef(join, [...structPrefix, key.parentElement.name]);
|
|
194
|
+
on.push(ref1, "=", ref2);
|
|
192
195
|
}
|
|
193
196
|
|
|
194
|
-
return on
|
|
195
|
-
}
|
|
197
|
+
return on;
|
|
198
|
+
};
|
|
196
199
|
|
|
197
200
|
const _newOnConditions = (csnElement, path, aliases) => {
|
|
198
201
|
if (csnElement.keys) {
|
|
199
|
-
return _foreignToOn(csnElement, path, aliases)
|
|
202
|
+
return _foreignToOn(csnElement, path, aliases);
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
return _args(csnElement, path, aliases)
|
|
203
|
-
}
|
|
205
|
+
return _args(csnElement, path, aliases);
|
|
206
|
+
};
|
|
204
207
|
|
|
205
|
-
const _getOnCond = (csnElement, path = [], aliases = { select:
|
|
206
|
-
const onCond = _newOnConditions(csnElement, path, aliases)
|
|
207
|
-
return [{ xpr: onCond }]
|
|
208
|
-
}
|
|
208
|
+
const _getOnCond = (csnElement, path = [], aliases = { select: "", join: "" }) => {
|
|
209
|
+
const onCond = _newOnConditions(csnElement, path, aliases);
|
|
210
|
+
return [{ xpr: onCond }];
|
|
211
|
+
};
|
package/lib/access.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
|
getRootEntity,
|
|
@@ -11,73 +11,74 @@ const {
|
|
|
11
11
|
addDataSubject,
|
|
12
12
|
addDataSubjectForDetailsEntity,
|
|
13
13
|
resolveDataSubjects
|
|
14
|
-
} = require(
|
|
14
|
+
} = require("./utils");
|
|
15
15
|
|
|
16
|
-
let audit
|
|
16
|
+
let audit;
|
|
17
17
|
|
|
18
18
|
const _processorFnAccess = (accessLogs, model, req) => {
|
|
19
19
|
return ({ row, key, element, plain }) => {
|
|
20
|
-
if (row.IsActiveEntity === false) return
|
|
20
|
+
if (row.IsActiveEntity === false) return;
|
|
21
21
|
|
|
22
|
-
const entity = getRootEntity(element)
|
|
22
|
+
const entity = getRootEntity(element);
|
|
23
23
|
|
|
24
24
|
// create or augment log entry
|
|
25
|
-
const entry = createLogEntry(accessLogs, entity, row)
|
|
25
|
+
const entry = createLogEntry(accessLogs, entity, row);
|
|
26
26
|
|
|
27
27
|
// process categories
|
|
28
28
|
for (const category of plain.categories) {
|
|
29
|
-
if (category ===
|
|
30
|
-
else if (category ===
|
|
31
|
-
else if (category ===
|
|
32
|
-
if (!entry.attributes.some(e => e.name === key)) entry.attributes.push({ name: key })
|
|
29
|
+
if (category === "ObjectID") addObjectID(entry, row, key);
|
|
30
|
+
else if (category === "DataSubjectID") addDataSubject(entry, row, key, entity);
|
|
31
|
+
else if (category === "IsPotentiallySensitive" && key in row) {
|
|
32
|
+
if (!entry.attributes.some((e) => e.name === key)) entry.attributes.push({ name: key });
|
|
33
33
|
// REVISIT: attribute vs. attachment?
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// add promise to determine data subject if a DataSubjectDetails entity
|
|
38
|
-
const semantics = entity[
|
|
38
|
+
const semantics = entity["@PersonalData.EntitySemantics"];
|
|
39
39
|
if (
|
|
40
|
-
(semantics ===
|
|
40
|
+
(semantics === "DataSubjectDetails" || semantics === "Other") &&
|
|
41
41
|
Object.keys(entry.data_subject.id).length === 0 // > id still an empty object -> promise not yet set
|
|
42
42
|
) {
|
|
43
|
-
addDataSubjectForDetailsEntity(row, entry, req, entity, model)
|
|
43
|
+
addDataSubjectForDetailsEntity(row, entry, req, entity, model);
|
|
44
44
|
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
47
|
|
|
48
48
|
const auditAccess = async function (data, req) {
|
|
49
|
-
if (!cds.env.requires[
|
|
49
|
+
if (!cds.env.requires["audit-log"].handle?.includes("READ")) return;
|
|
50
50
|
|
|
51
|
-
if (typeof data !==
|
|
51
|
+
if (typeof data !== "object" || data == null) return;
|
|
52
52
|
|
|
53
|
-
const mock = Object.assign({ name: req.target._service.name, model: this.model })
|
|
54
|
-
const template = getTemplate(
|
|
55
|
-
if (!template.elements.size) return
|
|
53
|
+
const mock = Object.assign({ name: req.target._service.name, model: this.model });
|
|
54
|
+
const template = getTemplate("personal_read", mock, req.target, { pick: getPick("READ") });
|
|
55
|
+
if (!template.elements.size) return;
|
|
56
56
|
|
|
57
|
-
const accessLogs = {}
|
|
58
|
-
const processFn = _processorFnAccess(accessLogs, this.model, req)
|
|
57
|
+
const accessLogs = {};
|
|
58
|
+
const processFn = _processorFnAccess(accessLogs, this.model, req);
|
|
59
59
|
// cds internal templating mechanism api changed in 8.2.0 -> polyfill
|
|
60
60
|
if (!template.process) {
|
|
61
|
-
module.exports._templateProcessor ??= require(
|
|
61
|
+
module.exports._templateProcessor ??= require("@sap/cds/libx/_runtime/common/utils/templateProcessor");
|
|
62
62
|
template.process = (data, processFn) => {
|
|
63
|
-
const _data = Array.isArray(data) ? data : [data]
|
|
64
|
-
_data.forEach(row => module.exports._templateProcessor({ processFn, row, template }))
|
|
65
|
-
}
|
|
63
|
+
const _data = Array.isArray(data) ? data : [data];
|
|
64
|
+
_data.forEach((row) => module.exports._templateProcessor({ processFn, row, template }));
|
|
65
|
+
};
|
|
66
66
|
}
|
|
67
|
-
template.process(data, processFn)
|
|
67
|
+
template.process(data, processFn);
|
|
68
68
|
|
|
69
|
-
for (const each of Object.keys(accessLogs))
|
|
70
|
-
|
|
69
|
+
for (const each of Object.keys(accessLogs))
|
|
70
|
+
if (!accessLogs[each].attributes.length) delete accessLogs[each];
|
|
71
|
+
if (!Object.keys(accessLogs).length) return;
|
|
71
72
|
|
|
72
|
-
await resolveDataSubjects(accessLogs, req)
|
|
73
|
-
const accesses = Object.values(accessLogs).filter(ele => ele.attributes.length)
|
|
74
|
-
if (!accesses.length) return
|
|
73
|
+
await resolveDataSubjects(accessLogs, req);
|
|
74
|
+
const accesses = Object.values(accessLogs).filter((ele) => ele.attributes.length);
|
|
75
|
+
if (!accesses.length) return;
|
|
75
76
|
|
|
76
|
-
audit = audit || (await cds.connect.to(
|
|
77
|
+
audit = audit || (await cds.connect.to("audit-log"));
|
|
77
78
|
|
|
78
|
-
await Promise.all(accesses.map(access => audit.log(
|
|
79
|
-
}
|
|
79
|
+
await Promise.all(accesses.map((access) => audit.log("SensitiveDataRead", access)));
|
|
80
|
+
};
|
|
80
81
|
|
|
81
82
|
module.exports = {
|
|
82
83
|
auditAccess
|
|
83
|
-
}
|
|
84
|
+
};
|