Skip to content

Commit fb4d660

Browse files
sebbi08mergify[bot]jkoenig134britsta
authored
Add validation for tags of Attributes (#436)
* feat: add valiadation of tags on attribute creation * chore: fix tests with invalid tags * feat: check for tags when accepting attribute requests * chore: remove duplicate test * chore: styling * chore: fix tags in tests * chore: fix tags in tests * chore: PR comments * chore: fix eslint * chore: pr comments * chore: fix tests * chore: pr comments * chore: improve tag validation function * chore: improve tag validation function * chore: make tag seperator a constant on the attributes controller * refactor: massively simplify validateTags method * refactor: use string instead of RegEx for expected error messages * feat: incorporate review comments * refactor: remove variable declaration for values used once * feat: add customTagPrefix to IdentityAttributeQuery and IQLQuery tests * feat: add function for validating tags of AttributeQueries as well * feat: apply tag validation to outgoing RequestItems and queries as well * refactor: more general error message fitting invalid tags of AttributeQueries as well * test: tag validation of outgoing CreateAttributeRequestItem * test: tag validation of outgoing ReadAttributeRequestItem * test: tag validation of outgoing ProposeAttributeRequestItem * refactor: use more descriptive test values for invalid tags * feat: apply tag validation to outgoing ShareAttributeRequestItems * test: tag validation of outgoing ShareAttributeRequestItem * fix: failing tests due to unchanged test values * refactor: use singular in test names when testing a single invalid tag * refactor: be more precise within test names * feat: incorporate review comments --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Julian König <[email protected]> Co-authored-by: Britta Stallknecht <[email protected]> Co-authored-by: Britta Stallknecht <[email protected]>
1 parent 154ce79 commit fb4d660

24 files changed

+897
-1767
lines changed

package-lock.json

Lines changed: 116 additions & 1589 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/consumption/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"@js-soft/docdb-access-mongo": "1.2.0",
7575
"@js-soft/node-logger": "1.2.0",
7676
"@nmshd/crypto": "2.1.0",
77-
"@types/lodash": "^4.17.16"
77+
"@types/lodash": "^4.17.16",
78+
"ts-mockito": "^2.6.1"
7879
},
7980
"publishConfig": {
8081
"access": "public",

packages/consumption/src/consumption/ConsumptionCoreErrors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ class Attributes {
266266
public setDefaultRepositoryAttributesIsDisabled() {
267267
return new CoreError("error.consumption.attributes.setDefaultRepositoryAttributesIsDisabled", "Setting default RepositoryAttributes is disabled for this Account.");
268268
}
269+
270+
public invalidTags(tags: string[]): ApplicationError {
271+
return new ApplicationError("error.consumption.attributes.invalidTags", `Detected invalidity of the following tags: '${tags.join("', '")}'.`);
272+
}
269273
}
270274

271275
class Requests {

packages/consumption/src/modules/attributes/AttributesController.ts

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
IdentityAttributeQuery,
88
IIdentityAttributeQuery,
99
IIQLQuery,
10+
IQLQuery,
1011
IRelationshipAttributeQuery,
1112
IThirdPartyRelationshipAttributeQuery,
13+
RelationshipAttribute,
1214
RelationshipAttributeJSON,
1315
RelationshipAttributeQuery,
1416
ThirdPartyRelationshipAttributeQuery,
@@ -35,16 +37,18 @@ import {
3537
ThirdPartyRelationshipAttributeSucceededEvent
3638
} from "./events";
3739
import { AttributeSuccessorParams, AttributeSuccessorParamsJSON, IAttributeSuccessorParams } from "./local/AttributeSuccessorParams";
38-
import { AttributeTagCollection } from "./local/AttributeTagCollection";
40+
import { AttributeTagCollection, IAttributeTag } from "./local/AttributeTagCollection";
3941
import { CreateRepositoryAttributeParams, ICreateRepositoryAttributeParams } from "./local/CreateRepositoryAttributeParams";
4042
import { CreateSharedLocalAttributeCopyParams, ICreateSharedLocalAttributeCopyParams } from "./local/CreateSharedLocalAttributeCopyParams";
41-
import { ICreateSharedLocalAttributeParams } from "./local/CreateSharedLocalAttributeParams";
43+
import { CreateSharedLocalAttributeParams, ICreateSharedLocalAttributeParams } from "./local/CreateSharedLocalAttributeParams";
4244
import { ILocalAttribute, LocalAttribute, LocalAttributeJSON } from "./local/LocalAttribute";
4345
import { LocalAttributeDeletionStatus } from "./local/LocalAttributeDeletionInfo";
4446
import { LocalAttributeShareInfo } from "./local/LocalAttributeShareInfo";
4547
import { IdentityAttributeQueryTranslator, RelationshipAttributeQueryTranslator, ThirdPartyRelationshipAttributeQueryTranslator } from "./local/QueryTranslator";
4648

4749
export class AttributesController extends ConsumptionBaseController {
50+
private static readonly TAG_SEPARATOR = "+%+";
51+
4852
private attributes: SynchronizedCollection;
4953
private attributeTagClient: TagClient;
5054

@@ -207,6 +211,10 @@ export class AttributesController extends ConsumptionBaseController {
207211
}
208212

209213
const parsedParams = CreateRepositoryAttributeParams.from(params);
214+
215+
const tagValidationResult = await this.validateTags(parsedParams.content);
216+
if (tagValidationResult.isError()) throw tagValidationResult.error;
217+
210218
let localAttribute = LocalAttribute.from({
211219
id: parsedParams.id ?? (await ConsumptionIds.attribute.generate()),
212220
createdAt: CoreDate.utc(),
@@ -318,6 +326,10 @@ export class AttributesController extends ConsumptionBaseController {
318326
}
319327

320328
public async createSharedLocalAttribute(params: ICreateSharedLocalAttributeParams): Promise<LocalAttribute> {
329+
const parsedParams = CreateSharedLocalAttributeParams.from(params);
330+
const tagValidationResult = await this.validateTags(parsedParams.content);
331+
if (tagValidationResult.isError()) throw tagValidationResult.error;
332+
321333
const shareInfo = LocalAttributeShareInfo.from({
322334
peer: params.peer,
323335
requestReference: params.requestReference,
@@ -907,6 +919,9 @@ export class AttributesController extends ConsumptionBaseController {
907919
return ValidationResult.error(ConsumptionCoreErrors.attributes.successorIsNotAValidAttribute(e));
908920
}
909921

922+
const tagValidationResult = await this.validateTags(parsedSuccessorParams.content);
923+
if (tagValidationResult.isError()) throw tagValidationResult.error;
924+
910925
const successor = LocalAttribute.from({
911926
id: CoreId.from(parsedSuccessorParams.id ?? "dummy"),
912927
content: parsedSuccessorParams.content,
@@ -1303,4 +1318,66 @@ export class AttributesController extends ConsumptionBaseController {
13031318
const backboneTagCollection = (await this.attributeTagClient.getTagCollection()).value;
13041319
return AttributeTagCollection.from(backboneTagCollection);
13051320
}
1321+
1322+
public async validateTags(attribute: IdentityAttribute | RelationshipAttribute): Promise<ValidationResult> {
1323+
if (attribute instanceof RelationshipAttribute) return ValidationResult.success();
1324+
if (!attribute.tags || attribute.tags.length === 0) return ValidationResult.success();
1325+
1326+
const tagCollection = await this.getAttributeTagCollection();
1327+
const invalidTags = [];
1328+
for (const tag of attribute.tags) {
1329+
if (!this.isValidTag(tag, tagCollection.tagsForAttributeValueTypes[attribute.toJSON().value["@type"]])) {
1330+
invalidTags.push(tag);
1331+
}
1332+
}
1333+
1334+
if (invalidTags.length > 0) {
1335+
return ValidationResult.error(ConsumptionCoreErrors.attributes.invalidTags(invalidTags));
1336+
}
1337+
1338+
return ValidationResult.success();
1339+
}
1340+
1341+
public async validateAttributeQueryTags(
1342+
attributeQuery: IdentityAttributeQuery | IQLQuery | RelationshipAttributeQuery | ThirdPartyRelationshipAttributeQuery
1343+
): Promise<ValidationResult> {
1344+
if (
1345+
(attributeQuery instanceof IQLQuery && !attributeQuery.attributeCreationHints) ||
1346+
attributeQuery instanceof RelationshipAttributeQuery ||
1347+
attributeQuery instanceof ThirdPartyRelationshipAttributeQuery
1348+
) {
1349+
return ValidationResult.success();
1350+
}
1351+
1352+
const attributeTags = attributeQuery instanceof IdentityAttributeQuery ? attributeQuery.tags : attributeQuery.attributeCreationHints!.tags;
1353+
if (!attributeTags || attributeTags.length === 0) return ValidationResult.success();
1354+
1355+
const attributeValueType = attributeQuery instanceof IdentityAttributeQuery ? attributeQuery.valueType : attributeQuery.attributeCreationHints!.valueType;
1356+
const tagCollection = await this.getAttributeTagCollection();
1357+
const invalidTags = [];
1358+
for (const tag of attributeTags) {
1359+
if (!this.isValidTag(tag, tagCollection.tagsForAttributeValueTypes[attributeValueType])) {
1360+
invalidTags.push(tag);
1361+
}
1362+
}
1363+
1364+
if (invalidTags.length > 0) {
1365+
return ValidationResult.error(ConsumptionCoreErrors.attributes.invalidTags(invalidTags));
1366+
}
1367+
1368+
return ValidationResult.success();
1369+
}
1370+
1371+
private isValidTag(tag: string, validTags: Record<string, IAttributeTag> | undefined): boolean {
1372+
const customTagPrefix = `x${AttributesController.TAG_SEPARATOR}`;
1373+
if (tag.toLowerCase().startsWith(customTagPrefix)) return true;
1374+
1375+
const tagParts = tag.split(AttributesController.TAG_SEPARATOR);
1376+
for (const part of tagParts) {
1377+
if (!validTags?.[part]) return false;
1378+
validTags = validTags[part].children;
1379+
}
1380+
1381+
return !validTags;
1382+
}
13061383
}

packages/consumption/src/modules/requests/itemProcessors/createAttribute/CreateAttributeRequestItemProcessor.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
2424
const ownerIsEmptyString = requestItem.attribute.owner.toString() === "";
2525

2626
if (requestItem.attribute instanceof IdentityAttribute) {
27-
if (recipientIsAttributeOwner || ownerIsEmptyString) {
28-
return ValidationResult.success();
29-
}
30-
3127
if (senderIsAttributeOwner) {
3228
return ValidationResult.error(
3329
ConsumptionCoreErrors.requests.invalidRequestItem(
@@ -36,36 +32,45 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
3632
);
3733
}
3834

39-
return ValidationResult.error(
40-
ConsumptionCoreErrors.requests.invalidRequestItem(
41-
"The owner of the provided IdentityAttribute for the `attribute` property can only be the address of the recipient or an empty string. The latter will default to the address of the recipient."
42-
)
43-
);
44-
}
35+
if (!(recipientIsAttributeOwner || ownerIsEmptyString)) {
36+
return ValidationResult.error(
37+
ConsumptionCoreErrors.requests.invalidRequestItem(
38+
"The owner of the provided IdentityAttribute for the `attribute` property can only be the address of the recipient or an empty string. The latter will default to the address of the recipient."
39+
)
40+
);
41+
}
4542

46-
if (!(recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString)) {
47-
return ValidationResult.error(
48-
ConsumptionCoreErrors.requests.invalidRequestItem(
49-
"The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, the address of the recipient or an empty string. The latter will default to the address of the recipient."
50-
)
51-
);
43+
const tagValidationResult = await this.consumptionController.attributes.validateTags(requestItem.attribute);
44+
if (tagValidationResult.isError()) {
45+
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
46+
}
5247
}
5348

54-
if (typeof recipient !== "undefined") {
55-
const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(
56-
requestItem.attribute.key,
57-
ownerIsEmptyString ? recipient : requestItem.attribute.owner,
58-
requestItem.attribute.value.toJSON()["@type"],
59-
recipient
60-
);
61-
62-
if (relationshipAttributesWithSameKey.length !== 0) {
49+
if (requestItem.attribute instanceof RelationshipAttribute) {
50+
if (!(recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString)) {
6351
return ValidationResult.error(
6452
ConsumptionCoreErrors.requests.invalidRequestItem(
65-
`The creation of the provided RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.attribute.key}', owner and value type.`
53+
"The owner of the provided RelationshipAttribute for the `attribute` property can only be the address of the sender, the address of the recipient or an empty string. The latter will default to the address of the recipient."
6654
)
6755
);
6856
}
57+
58+
if (typeof recipient !== "undefined") {
59+
const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(
60+
requestItem.attribute.key,
61+
ownerIsEmptyString ? recipient : requestItem.attribute.owner,
62+
requestItem.attribute.value.toJSON()["@type"],
63+
recipient
64+
);
65+
66+
if (relationshipAttributesWithSameKey.length !== 0) {
67+
return ValidationResult.error(
68+
ConsumptionCoreErrors.requests.invalidRequestItem(
69+
`The creation of the provided RelationshipAttribute cannot be requested because there is already a RelationshipAttribute in the context of this Relationship with the same key '${requestItem.attribute.key}', owner and value type.`
70+
)
71+
);
72+
}
73+
}
6974
}
7075

7176
return ValidationResult.success();
@@ -97,6 +102,11 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
97102
}
98103
}
99104

105+
const tagValidationResult = await this.consumptionController.attributes.validateTags(requestItem.attribute);
106+
if (tagValidationResult.isError()) {
107+
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
108+
}
109+
100110
return ValidationResult.success();
101111
}
102112

packages/consumption/src/modules/requests/itemProcessors/proposeAttribute/ProposeAttributeRequestItemProcessor.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ import { AcceptProposeAttributeRequestItemParameters, AcceptProposeAttributeRequ
2626

2727
export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProcessor<ProposeAttributeRequestItem, AcceptProposeAttributeRequestItemParametersJSON> {
2828
public override async canCreateOutgoingRequestItem(requestItem: ProposeAttributeRequestItem, _request: Request, recipient?: CoreAddress): Promise<ValidationResult> {
29-
const queryValidationResult = this.validateQuery(requestItem, recipient);
29+
const queryValidationResult = await this.validateQuery(requestItem, recipient);
3030
if (queryValidationResult.isError()) {
3131
return queryValidationResult;
3232
}
3333

34-
const attributeValidationResult = ProposeAttributeRequestItemProcessor.validateAttribute(requestItem.attribute);
34+
const attributeValidationResult = await this.validateAttribute(requestItem.attribute);
3535
if (attributeValidationResult.isError()) {
3636
return attributeValidationResult;
3737
}
@@ -66,7 +66,7 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
6666
return ValidationResult.success();
6767
}
6868

69-
private static validateAttribute(attribute: IdentityAttribute | RelationshipAttribute) {
69+
private async validateAttribute(attribute: IdentityAttribute | RelationshipAttribute) {
7070
if (attribute.owner.toString() !== "") {
7171
return ValidationResult.error(
7272
ConsumptionCoreErrors.requests.invalidRequestItem(
@@ -75,10 +75,15 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
7575
);
7676
}
7777

78+
const tagValidationResult = await this.consumptionController.attributes.validateTags(attribute);
79+
if (tagValidationResult.isError()) {
80+
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
81+
}
82+
7883
return ValidationResult.success();
7984
}
8085

81-
private validateQuery(requestItem: ProposeAttributeRequestItem, recipient?: CoreAddress) {
86+
private async validateQuery(requestItem: ProposeAttributeRequestItem, recipient?: CoreAddress) {
8287
const commonQueryValidationResult = validateQuery(requestItem.query, this.currentIdentityAddress, recipient);
8388
if (commonQueryValidationResult.isError()) {
8489
return commonQueryValidationResult;
@@ -92,6 +97,11 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
9297
);
9398
}
9499

100+
const tagValidationResult = await this.consumptionController.attributes.validateAttributeQueryTags(requestItem.query);
101+
if (tagValidationResult.isError()) {
102+
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
103+
}
104+
95105
return ValidationResult.success();
96106
}
97107

@@ -197,6 +207,11 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
197207
}
198208
}
199209

210+
const tagValidationResult = await this.consumptionController.attributes.validateTags(attribute);
211+
if (tagValidationResult.isError()) {
212+
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidAcceptParameters(tagValidationResult.error.message));
213+
}
214+
200215
return ValidationResult.success();
201216
}
202217

packages/consumption/src/modules/requests/itemProcessors/readAttribute/ReadAttributeRequestItemProcessor.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { AcceptReadAttributeRequestItemParameters, AcceptReadAttributeRequestIte
2929

3030
export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcessor<ReadAttributeRequestItem, AcceptReadAttributeRequestItemParametersJSON> {
3131
public override async canCreateOutgoingRequestItem(requestItem: ReadAttributeRequestItem, _request: Request, recipient?: CoreAddress): Promise<ValidationResult> {
32-
const queryValidationResult = this.validateQuery(requestItem, recipient);
32+
const queryValidationResult = await this.validateQuery(requestItem, recipient);
3333
if (queryValidationResult.isError()) {
3434
return queryValidationResult;
3535
}
@@ -55,7 +55,7 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess
5555
return ValidationResult.success();
5656
}
5757

58-
private validateQuery(requestItem: ReadAttributeRequestItem, recipient?: CoreAddress) {
58+
private async validateQuery(requestItem: ReadAttributeRequestItem, recipient?: CoreAddress) {
5959
const commonQueryValidationResult = validateQuery(requestItem.query, this.currentIdentityAddress, recipient);
6060
if (commonQueryValidationResult.isError()) {
6161
return commonQueryValidationResult;
@@ -74,6 +74,11 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess
7474
}
7575
}
7676

77+
const tagValidationResult = await this.consumptionController.attributes.validateAttributeQueryTags(requestItem.query);
78+
if (tagValidationResult.isError()) {
79+
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
80+
}
81+
7782
return ValidationResult.success();
7883
}
7984

@@ -270,6 +275,11 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess
270275
);
271276
}
272277

278+
const tagValidationResult = await this.consumptionController.attributes.validateTags(attribute);
279+
if (tagValidationResult.isError()) {
280+
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidAcceptParameters(tagValidationResult.error.message));
281+
}
282+
273283
return ValidationResult.success();
274284
}
275285

0 commit comments

Comments
 (0)