Skip to content

Commit

Permalink
Add validation for tags of Attributes (#436)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
5 people authored Mar 6, 2025
1 parent 154ce79 commit fb4d660
Show file tree
Hide file tree
Showing 24 changed files with 897 additions and 1,767 deletions.
1,705 changes: 116 additions & 1,589 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/consumption/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"@js-soft/docdb-access-mongo": "1.2.0",
"@js-soft/node-logger": "1.2.0",
"@nmshd/crypto": "2.1.0",
"@types/lodash": "^4.17.16"
"@types/lodash": "^4.17.16",
"ts-mockito": "^2.6.1"
},
"publishConfig": {
"access": "public",
Expand Down
4 changes: 4 additions & 0 deletions packages/consumption/src/consumption/ConsumptionCoreErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ class Attributes {
public setDefaultRepositoryAttributesIsDisabled() {
return new CoreError("error.consumption.attributes.setDefaultRepositoryAttributesIsDisabled", "Setting default RepositoryAttributes is disabled for this Account.");
}

public invalidTags(tags: string[]): ApplicationError {
return new ApplicationError("error.consumption.attributes.invalidTags", `Detected invalidity of the following tags: '${tags.join("', '")}'.`);
}
}

class Requests {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
IdentityAttributeQuery,
IIdentityAttributeQuery,
IIQLQuery,
IQLQuery,
IRelationshipAttributeQuery,
IThirdPartyRelationshipAttributeQuery,
RelationshipAttribute,
RelationshipAttributeJSON,
RelationshipAttributeQuery,
ThirdPartyRelationshipAttributeQuery,
Expand All @@ -35,16 +37,18 @@ import {
ThirdPartyRelationshipAttributeSucceededEvent
} from "./events";
import { AttributeSuccessorParams, AttributeSuccessorParamsJSON, IAttributeSuccessorParams } from "./local/AttributeSuccessorParams";
import { AttributeTagCollection } from "./local/AttributeTagCollection";
import { AttributeTagCollection, IAttributeTag } from "./local/AttributeTagCollection";
import { CreateRepositoryAttributeParams, ICreateRepositoryAttributeParams } from "./local/CreateRepositoryAttributeParams";
import { CreateSharedLocalAttributeCopyParams, ICreateSharedLocalAttributeCopyParams } from "./local/CreateSharedLocalAttributeCopyParams";
import { ICreateSharedLocalAttributeParams } from "./local/CreateSharedLocalAttributeParams";
import { CreateSharedLocalAttributeParams, ICreateSharedLocalAttributeParams } from "./local/CreateSharedLocalAttributeParams";
import { ILocalAttribute, LocalAttribute, LocalAttributeJSON } from "./local/LocalAttribute";
import { LocalAttributeDeletionStatus } from "./local/LocalAttributeDeletionInfo";
import { LocalAttributeShareInfo } from "./local/LocalAttributeShareInfo";
import { IdentityAttributeQueryTranslator, RelationshipAttributeQueryTranslator, ThirdPartyRelationshipAttributeQueryTranslator } from "./local/QueryTranslator";

export class AttributesController extends ConsumptionBaseController {
private static readonly TAG_SEPARATOR = "+%+";

private attributes: SynchronizedCollection;
private attributeTagClient: TagClient;

Expand Down Expand Up @@ -207,6 +211,10 @@ export class AttributesController extends ConsumptionBaseController {
}

const parsedParams = CreateRepositoryAttributeParams.from(params);

const tagValidationResult = await this.validateTags(parsedParams.content);
if (tagValidationResult.isError()) throw tagValidationResult.error;

let localAttribute = LocalAttribute.from({
id: parsedParams.id ?? (await ConsumptionIds.attribute.generate()),
createdAt: CoreDate.utc(),
Expand Down Expand Up @@ -318,6 +326,10 @@ export class AttributesController extends ConsumptionBaseController {
}

public async createSharedLocalAttribute(params: ICreateSharedLocalAttributeParams): Promise<LocalAttribute> {
const parsedParams = CreateSharedLocalAttributeParams.from(params);
const tagValidationResult = await this.validateTags(parsedParams.content);
if (tagValidationResult.isError()) throw tagValidationResult.error;

const shareInfo = LocalAttributeShareInfo.from({
peer: params.peer,
requestReference: params.requestReference,
Expand Down Expand Up @@ -907,6 +919,9 @@ export class AttributesController extends ConsumptionBaseController {
return ValidationResult.error(ConsumptionCoreErrors.attributes.successorIsNotAValidAttribute(e));
}

const tagValidationResult = await this.validateTags(parsedSuccessorParams.content);
if (tagValidationResult.isError()) throw tagValidationResult.error;

const successor = LocalAttribute.from({
id: CoreId.from(parsedSuccessorParams.id ?? "dummy"),
content: parsedSuccessorParams.content,
Expand Down Expand Up @@ -1303,4 +1318,66 @@ export class AttributesController extends ConsumptionBaseController {
const backboneTagCollection = (await this.attributeTagClient.getTagCollection()).value;
return AttributeTagCollection.from(backboneTagCollection);
}

public async validateTags(attribute: IdentityAttribute | RelationshipAttribute): Promise<ValidationResult> {
if (attribute instanceof RelationshipAttribute) return ValidationResult.success();
if (!attribute.tags || attribute.tags.length === 0) return ValidationResult.success();

const tagCollection = await this.getAttributeTagCollection();
const invalidTags = [];
for (const tag of attribute.tags) {
if (!this.isValidTag(tag, tagCollection.tagsForAttributeValueTypes[attribute.toJSON().value["@type"]])) {
invalidTags.push(tag);
}
}

if (invalidTags.length > 0) {
return ValidationResult.error(ConsumptionCoreErrors.attributes.invalidTags(invalidTags));
}

return ValidationResult.success();
}

public async validateAttributeQueryTags(
attributeQuery: IdentityAttributeQuery | IQLQuery | RelationshipAttributeQuery | ThirdPartyRelationshipAttributeQuery
): Promise<ValidationResult> {
if (
(attributeQuery instanceof IQLQuery && !attributeQuery.attributeCreationHints) ||
attributeQuery instanceof RelationshipAttributeQuery ||
attributeQuery instanceof ThirdPartyRelationshipAttributeQuery
) {
return ValidationResult.success();
}

const attributeTags = attributeQuery instanceof IdentityAttributeQuery ? attributeQuery.tags : attributeQuery.attributeCreationHints!.tags;
if (!attributeTags || attributeTags.length === 0) return ValidationResult.success();

const attributeValueType = attributeQuery instanceof IdentityAttributeQuery ? attributeQuery.valueType : attributeQuery.attributeCreationHints!.valueType;
const tagCollection = await this.getAttributeTagCollection();
const invalidTags = [];
for (const tag of attributeTags) {
if (!this.isValidTag(tag, tagCollection.tagsForAttributeValueTypes[attributeValueType])) {
invalidTags.push(tag);
}
}

if (invalidTags.length > 0) {
return ValidationResult.error(ConsumptionCoreErrors.attributes.invalidTags(invalidTags));
}

return ValidationResult.success();
}

private isValidTag(tag: string, validTags: Record<string, IAttributeTag> | undefined): boolean {
const customTagPrefix = `x${AttributesController.TAG_SEPARATOR}`;
if (tag.toLowerCase().startsWith(customTagPrefix)) return true;

const tagParts = tag.split(AttributesController.TAG_SEPARATOR);
for (const part of tagParts) {
if (!validTags?.[part]) return false;
validTags = validTags[part].children;
}

return !validTags;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
const ownerIsEmptyString = requestItem.attribute.owner.toString() === "";

if (requestItem.attribute instanceof IdentityAttribute) {
if (recipientIsAttributeOwner || ownerIsEmptyString) {
return ValidationResult.success();
}

if (senderIsAttributeOwner) {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
Expand All @@ -36,36 +32,45 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
);
}

return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
"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."
)
);
}
if (!(recipientIsAttributeOwner || ownerIsEmptyString)) {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
"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."
)
);
}

if (!(recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString)) {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
"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."
)
);
const tagValidationResult = await this.consumptionController.attributes.validateTags(requestItem.attribute);
if (tagValidationResult.isError()) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
}
}

if (typeof recipient !== "undefined") {
const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(
requestItem.attribute.key,
ownerIsEmptyString ? recipient : requestItem.attribute.owner,
requestItem.attribute.value.toJSON()["@type"],
recipient
);

if (relationshipAttributesWithSameKey.length !== 0) {
if (requestItem.attribute instanceof RelationshipAttribute) {
if (!(recipientIsAttributeOwner || senderIsAttributeOwner || ownerIsEmptyString)) {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
`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.`
"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."
)
);
}

if (typeof recipient !== "undefined") {
const relationshipAttributesWithSameKey = await this.consumptionController.attributes.getRelationshipAttributesOfValueTypeToPeerWithGivenKeyAndOwner(
requestItem.attribute.key,
ownerIsEmptyString ? recipient : requestItem.attribute.owner,
requestItem.attribute.value.toJSON()["@type"],
recipient
);

if (relationshipAttributesWithSameKey.length !== 0) {
return ValidationResult.error(
ConsumptionCoreErrors.requests.invalidRequestItem(
`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.`
)
);
}
}
}

return ValidationResult.success();
Expand Down Expand Up @@ -97,6 +102,11 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
}
}

const tagValidationResult = await this.consumptionController.attributes.validateTags(requestItem.attribute);
if (tagValidationResult.isError()) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
}

return ValidationResult.success();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ import { AcceptProposeAttributeRequestItemParameters, AcceptProposeAttributeRequ

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

const attributeValidationResult = ProposeAttributeRequestItemProcessor.validateAttribute(requestItem.attribute);
const attributeValidationResult = await this.validateAttribute(requestItem.attribute);
if (attributeValidationResult.isError()) {
return attributeValidationResult;
}
Expand Down Expand Up @@ -66,7 +66,7 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
return ValidationResult.success();
}

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

const tagValidationResult = await this.consumptionController.attributes.validateTags(attribute);
if (tagValidationResult.isError()) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
}

return ValidationResult.success();
}

private validateQuery(requestItem: ProposeAttributeRequestItem, recipient?: CoreAddress) {
private async validateQuery(requestItem: ProposeAttributeRequestItem, recipient?: CoreAddress) {
const commonQueryValidationResult = validateQuery(requestItem.query, this.currentIdentityAddress, recipient);
if (commonQueryValidationResult.isError()) {
return commonQueryValidationResult;
Expand All @@ -92,6 +97,11 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
);
}

const tagValidationResult = await this.consumptionController.attributes.validateAttributeQueryTags(requestItem.query);
if (tagValidationResult.isError()) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
}

return ValidationResult.success();
}

Expand Down Expand Up @@ -197,6 +207,11 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
}
}

const tagValidationResult = await this.consumptionController.attributes.validateTags(attribute);
if (tagValidationResult.isError()) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidAcceptParameters(tagValidationResult.error.message));
}

return ValidationResult.success();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { AcceptReadAttributeRequestItemParameters, AcceptReadAttributeRequestIte

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

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

const tagValidationResult = await this.consumptionController.attributes.validateAttributeQueryTags(requestItem.query);
if (tagValidationResult.isError()) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem(tagValidationResult.error.message));
}

return ValidationResult.success();
}

Expand Down Expand Up @@ -270,6 +275,11 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess
);
}

const tagValidationResult = await this.consumptionController.attributes.validateTags(attribute);
if (tagValidationResult.isError()) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidAcceptParameters(tagValidationResult.error.message));
}

return ValidationResult.success();
}

Expand Down
Loading

0 comments on commit fb4d660

Please sign in to comment.