Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2b1baac
feat: add valiadation of tags on attribute creation
sebbi08 Feb 20, 2025
c479870
chore: fix tests with invalid tags
sebbi08 Feb 21, 2025
b850c5e
feat: check for tags when accepting attribute requests
sebbi08 Feb 25, 2025
565b43a
Merge branch 'main' into feature/add_tag_validation
sebbi08 Feb 25, 2025
14a6adc
chore: remove duplicate test
sebbi08 Feb 25, 2025
16eb3be
chore: styling
sebbi08 Feb 25, 2025
efb1fa7
chore: fix tags in tests
sebbi08 Feb 25, 2025
843b513
chore: fix tags in tests
sebbi08 Feb 25, 2025
f9cbc22
chore: PR comments
sebbi08 Feb 26, 2025
d44c988
chore: fix eslint
sebbi08 Feb 26, 2025
e505bb2
chore: pr comments
sebbi08 Feb 26, 2025
cad24af
Merge branch 'main' into feature/add_tag_validation
mergify[bot] Feb 26, 2025
94b6b75
chore: fix tests
sebbi08 Feb 26, 2025
2cfbb36
Merge branch 'main' into feature/add_tag_validation
mergify[bot] Feb 26, 2025
aee12c5
chore: pr comments
sebbi08 Feb 27, 2025
6772b11
chore: improve tag validation function
sebbi08 Feb 28, 2025
05f9659
Merge branch 'main' into feature/add_tag_validation
sebbi08 Feb 28, 2025
9b6bf7f
chore: improve tag validation function
sebbi08 Feb 28, 2025
5d7f45b
chore: make tag seperator a constant on the attributes controller
sebbi08 Feb 28, 2025
79ba76c
refactor: massively simplify validateTags method
jkoenig134 Feb 28, 2025
e075743
Merge branch 'main' into feature/add_tag_validation
mergify[bot] Feb 28, 2025
c0ccc12
Merge branch 'main' into feature/add_tag_validation
mergify[bot] Mar 3, 2025
277a760
Merge branch 'release/v7' into feature/add_tag_validation
mergify[bot] Mar 4, 2025
4ed6196
Merge branch 'release/v7' into feature/add_tag_validation
jkoenig134 Mar 5, 2025
a089cf1
Merge branch 'release/v7' into feature/add_tag_validation
mergify[bot] Mar 5, 2025
f7045ea
refactor: use string instead of RegEx for expected error messages
britsta Mar 5, 2025
bb46848
feat: incorporate review comments
britsta Mar 5, 2025
f3e6905
Merge branch 'release/v7' into feature/add_tag_validation
mergify[bot] Mar 5, 2025
fbd6b93
refactor: remove variable declaration for values used once
britsta Mar 5, 2025
48f4ed7
feat: add customTagPrefix to IdentityAttributeQuery and IQLQuery tests
britsta Mar 5, 2025
d5a6fba
feat: add function for validating tags of AttributeQueries as well
britsta Mar 5, 2025
06ff57d
feat: apply tag validation to outgoing RequestItems and queries as well
britsta Mar 5, 2025
bf7768e
refactor: more general error message fitting invalid tags of Attribut…
britsta Mar 5, 2025
5793f6a
test: tag validation of outgoing CreateAttributeRequestItem
britsta Mar 5, 2025
3fa7a6f
test: tag validation of outgoing ReadAttributeRequestItem
britsta Mar 5, 2025
19b9286
test: tag validation of outgoing ProposeAttributeRequestItem
britsta Mar 5, 2025
58cd247
refactor: use more descriptive test values for invalid tags
britsta Mar 5, 2025
38905be
feat: apply tag validation to outgoing ShareAttributeRequestItems
britsta Mar 5, 2025
4975cfe
test: tag validation of outgoing ShareAttributeRequestItem
britsta Mar 5, 2025
5e324a3
Merge branch 'feature/add_tag_validation' of github.com:nmshd/runtime…
britsta Mar 5, 2025
471376f
fix: failing tests due to unchanged test values
britsta Mar 5, 2025
f51ed21
refactor: use singular in test names when testing a single invalid tag
britsta Mar 5, 2025
39bb5f8
refactor: be more precise within test names
britsta Mar 5, 2025
d42c0ee
Merge branch 'release/v7' into feature/add_tag_validation
mergify[bot] Mar 6, 2025
13906ec
Merge branch 'release/v7' into feature/add_tag_validation
britsta Mar 6, 2025
b44287a
feat: incorporate review comments
britsta Mar 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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