Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/consumption/src/consumption/ConsumptionCoreErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ class Attributes {
public invalidTags(tags: string[]): ApplicationError {
return new ApplicationError("error.consumption.attributes.invalidTags", `Detected invalidity of the following tags: '${tags.join("', '")}'.`);
}

public forbiddenCharactersInAttribute(message: string) {
return new CoreError("error.consumption.attributes.forbiddenCharactersInAttribute", message);
}
}

class Requests {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ export class AttributesController extends ConsumptionBaseController {
};
parsedParams.content = IdentityAttribute.from(trimmedAttribute);

if (!this.validateAttributeCharacters(parsedParams.content)) {
throw ConsumptionCoreErrors.attributes.forbiddenCharactersInAttribute("The Attribute contains forbidden characters.");
}

let localAttribute = LocalAttribute.from({
id: parsedParams.id ?? (await ConsumptionIds.attribute.generate()),
createdAt: CoreDate.utc(),
Expand Down Expand Up @@ -1011,6 +1015,10 @@ export class AttributesController extends ConsumptionBaseController {
return ValidationResult.error(ConsumptionCoreErrors.attributes.setPredecessorIdDoesNotMatchActualPredecessorId());
}

if (!this.validateAttributeCharacters(successor.content)) {
return ValidationResult.error(ConsumptionCoreErrors.attributes.forbiddenCharactersInAttribute("The successor contains forbidden characters."));
}

return ValidationResult.success();
}

Expand Down Expand Up @@ -1515,6 +1523,17 @@ export class AttributesController extends ConsumptionBaseController {
return !validTags;
}

public validateAttributeCharacters(attribute: IdentityAttribute | RelationshipAttribute): boolean {
const regex =
/^([\u0009-\u000A]|\u000D|[ -~]|[ -¬]|[®-ž]|[Ƈ-ƈ]|Ə|Ɨ|[Ơ-ơ]|[Ư-ư]|Ʒ|[Ǎ-ǜ]|[Ǟ-ǟ]|[Ǣ-ǰ]|[Ǵ-ǵ]|[Ǹ-ǿ]|[Ȓ-ȓ]|[Ș-ț]|[Ȟ-ȟ]|[ȧ-ȳ]|ə|ɨ|ʒ|[ʹ-ʺ]|[ʾ-ʿ]|ˈ|ˌ|[Ḃ-ḃ]|[Ḇ-ḇ]|[Ḋ-ḑ]|ḗ|[Ḝ-ḫ]|[ḯ-ḷ]|[Ḻ-ḻ]|[Ṁ-ṉ]|[Ṓ-ṛ]|[Ṟ-ṣ]|[Ṫ-ṯ]|[Ẁ-ẇ]|[Ẍ-ẗ]|ẞ|[Ạ-ỹ]|’|‡|€|A̋|C(̀|̄|̆|̈|̕|̣|̦|̨̆)|D̂|F(̀|̄)|G̀|H(̄|̦|̱)|J(́|̌)|K(̀|̂|̄|̇|̕|̛|̦|͟H|͟h)|L(̂|̥|̥̄|̦)|M(̀|̂|̆|̐)|N(̂|̄|̆|̦)|P(̀|̄|̕|̣)|R(̆|̥|̥̄)|S(̀|̄|̛̄|̱)|T(̀|̄|̈|̕|̛)|U̇|Z(̀|̄|̆|̈|̧)|a̋|c(̀|̄|̆|̈|̕|̣|̦|̨̆)|d̂|f(̀|̄)|g̀|h(̄|̦)|j́|k(̀|̂|̄|̇|̕|̛|̦|͟h)|l(̂|̥|̥̄|̦)|m(̀|̂|̆|̐)|n(̂|̄|̆|̦)|p(̀|̄|̕|̣)|r(̆|̥|̥̄)|s(̀|̄|̛̄|̱)|t(̀|̄|̕|̛)|u̇|z(̀|̄|̆|̈|̧)|Ç̆|Û̄|ç̆|û̄|ÿ́|Č(̕|̣)|č(̕|̣)|ē̍|Ī́|ī́|ō̍|Ž(̦|̧)|ž(̦|̧)|Ḳ̄|ḳ̄|Ṣ̄|ṣ̄|Ṭ̄|ṭ̄|Ạ̈|ạ̈|Ọ̈|ọ̈|Ụ(̄|̈)|ụ(̄|̈))*$/;
if (attribute instanceof IdentityAttribute) {
return Object.values(attribute.value.toJSON()).every((entry) => typeof entry !== "string" || regex.test(entry));
}

const nonDescriptiveEntries = Object.entries(attribute.value.toJSON()).filter((entry) => !["title", "description"].includes(entry[0]));
return nonDescriptiveEntries.every((entry) => typeof entry[1] !== "string" || regex.test(entry[1]));
}

public async setAttributeDeletionInfoOfDeletionProposedRelationship(relationshipId: CoreId): Promise<void> {
const relationship = await this.parent.accountController.relationships.getRelationship(relationshipId);
if (!relationship) throw TransportCoreErrors.general.recordNotFound(Relationship, relationshipId.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
const senderIsAttributeOwner = requestItem.attribute.owner.equals(this.currentIdentityAddress);
const ownerIsEmptyString = requestItem.attribute.owner.toString() === "";

if (!this.consumptionController.attributes.validateAttributeCharacters(requestItem.attribute)) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem("The Attribute contains forbidden characters."));
}

if (requestItem.attribute instanceof IdentityAttribute) {
if (senderIsAttributeOwner) {
return ValidationResult.error(
Expand Down Expand Up @@ -78,6 +82,10 @@ export class CreateAttributeRequestItemProcessor extends GenericRequestItemProce
}

public override async canAccept(requestItem: CreateAttributeRequestItem, _params: AcceptRequestItemParametersJSON, requestInfo: LocalRequestInfo): Promise<ValidationResult> {
if (!this.consumptionController.attributes.validateAttributeCharacters(requestItem.attribute)) {
throw ConsumptionCoreErrors.attributes.forbiddenCharactersInAttribute("The Attribute contains forbidden characters.");
}

if (requestItem.attribute instanceof RelationshipAttribute) {
const ownerIsEmptyString = requestItem.attribute.owner.toString() === "";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
)
);
}
if (!this.consumptionController.attributes.validateAttributeCharacters(attribute)) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidRequestItem("The Attribute contains forbidden characters."));
}

const tagValidationResult = await this.consumptionController.attributes.validateTagsOfAttribute(attribute);
if (tagValidationResult.isError()) {
Expand Down Expand Up @@ -191,6 +194,10 @@ export class ProposeAttributeRequestItemProcessor extends GenericRequestItemProc
);
}

if (!this.consumptionController.attributes.validateAttributeCharacters(attribute)) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidAcceptParameters("The Attribute contains forbidden characters."));
}

const answerToQueryValidationResult = validateAttributeMatchesWithQuery(requestItem.query, attribute, this.currentIdentityAddress, requestInfo.peer);
if (answerToQueryValidationResult.isError()) return answerToQueryValidationResult;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ export class ReadAttributeRequestItemProcessor extends GenericRequestItemProcess
);
}

if (!this.consumptionController.attributes.validateAttributeCharacters(attribute)) {
return ValidationResult.error(ConsumptionCoreErrors.requests.invalidAcceptParameters("The Attribute contains forbidden characters."));
}

const answerToQueryValidationResult = validateAttributeMatchesWithQuery(requestItem.query, attribute, this.currentIdentityAddress, requestInfo.peer);
if (answerToQueryValidationResult.isError()) return answerToQueryValidationResult;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
IIQLQuery,
IRelationshipAttributeQuery,
Nationality,
ProprietaryString,
RelationshipAttribute,
RelationshipAttributeConfidentiality,
Street,
Expand Down Expand Up @@ -110,6 +111,22 @@ describe("AttributesController", function () {
mockEventBus.expectPublishedEvents(AttributeCreatedEvent);
});

test("should not create a new attribute with a forbidden character", async function () {
const params: ICreateRepositoryAttributeParams = {
content: IdentityAttribute.from({
value: {
"@type": "DisplayName",
value: "aDisplayName😀"
},
owner: consumptionController.accountController.identity.address
})
};

await expect(consumptionController.attributes.createRepositoryAttribute(params)).rejects.toThrow(
"error.consumption.attributes.forbiddenCharactersInAttribute: 'The Attribute contains forbidden characters.'"
);
});

test("should trim whitespace for a RepositoryAttribute", async function () {
const params: ICreateRepositoryAttributeParams = {
content: IdentityAttribute.from({
Expand Down Expand Up @@ -1278,6 +1295,35 @@ describe("AttributesController", function () {

describe("succeed Attributes", function () {
describe("Common validator", function () {
test("should catch a forbidden character in the successor", async function () {
const predecessor = await consumptionController.attributes.createRepositoryAttribute({
content: IdentityAttribute.from({
value: {
"@type": "GivenName",
value: "aGivenName"
},
owner: consumptionController.accountController.identity.address,
tags: ["x:aTag"]
})
});

const successorData: IAttributeSuccessorParams = {
content: IdentityAttribute.from({
value: {
"@type": "GivenName",
value: "aGivenName😀"
},
owner: consumptionController.accountController.identity.address,
tags: ["x:aTag"]
})
};

const validationResult = await consumptionController.attributes.validateAttributeSuccessionCommon(predecessor.id, successorData);
expect(validationResult).errorValidationResult({
code: "error.consumption.attributes.forbiddenCharactersInAttribute"
});
});

test("should catch if content doesn't change", async function () {
const predecessor = await consumptionController.attributes.createRepositoryAttribute({
content: IdentityAttribute.from({
Expand Down Expand Up @@ -3682,4 +3728,43 @@ describe("AttributesController", function () {
verify(attributesControllerSpy["setTagCollection"](anything())).twice();
});
});

describe("validate attribute values", function () {
test("should catch forbidden characters in an IdentityAttribute", function () {
expect(
consumptionController.attributes.validateAttributeCharacters(
IdentityAttribute.from({
owner: CoreAddress.from("anAddress"),
value: City.from({ value: "aCity😀" })
})
)
).toBe(false);
});

test("should catch forbidden characters in a RelationshipAttribute", function () {
expect(
consumptionController.attributes.validateAttributeCharacters(
RelationshipAttribute.from({
key: "aKey",
owner: CoreAddress.from("anAddress"),
confidentiality: RelationshipAttributeConfidentiality.Public,
value: ProprietaryString.from({ title: "aTitle", value: "aProprietaryStringValue😀" })
})
)
).toBe(false);
});

test("should allow all characters in a RelationshipAttribute's title and description", function () {
expect(
consumptionController.attributes.validateAttributeCharacters(
RelationshipAttribute.from({
key: "aKey",
owner: CoreAddress.from("anAddress"),
confidentiality: RelationshipAttributeConfidentiality.Public,
value: ProprietaryString.from({ title: "aTitle😀", value: "aProprietaryStringValue", description: "aDescription😀" })
})
)
).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ describe("CreateAttributeRequestItemProcessor", function () {
});
});

test("returns Error when passing an IdentityAttribute with a forbidden character", async function () {
const identityAttributeOfRecipient = TestObjectFactory.createIdentityAttribute({
owner: TestIdentity.PEER,
value: GivenName.from({ value: "aGivenName😀" })
});

await When.iCallCanCreateOutgoingRequestItemWith({ attribute: identityAttributeOfRecipient });
await Then.theCanCreateResultShouldBeAnErrorWith({
code: "error.consumption.requests.invalidRequestItem",
message: "The Attribute contains forbidden characters."
});
});

test("returns Error when passing a RelationshipAttribute with same key as an already existing RelationshipAttribute of this Relationship", async function () {
const relationshipAttributeOfSender = TestObjectFactory.createRelationshipAttribute({
owner: TestIdentity.CURRENT_IDENTITY,
Expand Down Expand Up @@ -276,6 +289,15 @@ describe("CreateAttributeRequestItemProcessor", function () {
await Then.theCanAcceptResultShouldBeASuccess();
});

test("cannot create an IdentityAttribute with a forbidden character", async function () {
await Given.aRequestItemWithAnIdentityAttribute({
attributeOwner: TestIdentity.PEER,
value: GivenName.from({ value: "aGivenName😀" })
});

await expect(When.iCallCanAccept()).rejects.toThrow("error.consumption.attributes.forbiddenCharactersInAttribute: 'The Attribute contains forbidden characters.'");
});

test("cannot create another RelationshipAttribute with same key", async function () {
const relationshipAttributeOfRecipient = TestObjectFactory.createRelationshipAttribute({
owner: TestIdentity.PEER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,28 @@ describe("ProposeAttributeRequestItemProcessor", function () {
expect(result).successfulValidationResult();
});

test("returns an error when passing a forbidden character", async () => {
const recipient = CoreAddress.from("Recipient");

const requestItem = ProposeAttributeRequestItem.from({
mustBeAccepted: false,
attribute: TestObjectFactory.createIdentityAttribute({
value: GivenName.fromAny({ value: "aGivenName😀" }),
owner: CoreAddress.from("")
}),
query: IdentityAttributeQuery.from({
valueType: "GivenName"
})
});

const result = await processor.canCreateOutgoingRequestItem(requestItem, Request.from({ items: [requestItem] }), recipient);

expect(result).errorValidationResult({
code: "error.consumption.requests.invalidRequestItem",
message: "The Attribute contains forbidden characters."
});
});

test("returns an error when passing anything other than an empty string as an owner into 'attribute'", async () => {
const requestItem = ProposeAttributeRequestItem.from({
mustBeAccepted: false,
Expand Down Expand Up @@ -485,6 +507,49 @@ describe("ProposeAttributeRequestItemProcessor", function () {
expect(result).successfulValidationResult();
});

test("returns an error when the attribute contains a forbidden character", async function () {
const sender = CoreAddress.from("Sender");
const recipient = accountController.identity.address;

const requestItem = ProposeAttributeRequestItem.from({
mustBeAccepted: true,
query: IdentityAttributeQuery.from({ valueType: "GivenName" }),
attribute: TestObjectFactory.createIdentityAttribute()
});
const requestId = await ConsumptionIds.request.generate();
const request = LocalRequest.from({
id: requestId,
createdAt: CoreDate.utc(),
isOwn: false,
peer: sender,
status: LocalRequestStatus.DecisionRequired,
content: Request.from({
id: requestId,
items: [requestItem]
}),
statusLog: []
});

const acceptParams: AcceptProposeAttributeRequestItemParametersWithNewAttributeJSON = {
accept: true,
attribute: {
"@type": "IdentityAttribute",
owner: recipient.toString(),
value: {
"@type": "GivenName",
value: "aGivenName😀"
}
}
};

const result = await processor.canAccept(requestItem, acceptParams, request);

expect(result).errorValidationResult({
code: "error.consumption.requests.invalidAcceptParameters",
message: "The Attribute contains forbidden characters."
});
});

test("returns an error when the given Attribute id does not exist", async function () {
const requestItem = ProposeAttributeRequestItem.from({
mustBeAccepted: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,46 @@ describe("ReadAttributeRequestItemProcessor", function () {
});
});

test("returns an error when the attribute contains a forbidden character", async function () {
const requestItem = ReadAttributeRequestItem.from({
mustBeAccepted: true,
query: IdentityAttributeQuery.from({ valueType: "GivenName" })
});
const requestId = await ConsumptionIds.request.generate();
const request = LocalRequest.from({
id: requestId,
createdAt: CoreDate.utc(),
isOwn: false,
peer: sender,
status: LocalRequestStatus.DecisionRequired,
content: Request.from({
id: requestId,
items: [requestItem]
}),
statusLog: []
});

const acceptParams: AcceptReadAttributeRequestItemParametersWithNewAttributeJSON = {
accept: true,
newAttribute: {
"@type": "IdentityAttribute",
owner: recipient.toString(),
value: {
"@type": "GivenName",
value: "aGivenName😀"
},
tags: ["aTag"]
}
};

const result = await processor.canAccept(requestItem, acceptParams, request);

expect(result).errorValidationResult({
code: "error.consumption.requests.invalidAcceptParameters",
message: "The Attribute contains forbidden characters."
});
});

test("returns an error trying to share the predecessor of an already shared Attribute", async function () {
const predecessorRepositoryAttribute = await consumptionController.attributes.createRepositoryAttribute({
content: TestObjectFactory.createIdentityAttribute({
Expand Down