From 585f1161a8b3155e3dfb540e6ae2db4e2ca4f092 Mon Sep 17 00:00:00 2001 From: Daniel Augusto de Melo Santos Date: Tue, 16 Feb 2021 15:41:29 -0300 Subject: [PATCH 1/2] [CIV-2716] Supporting new aggregate tags on DSR --- src/ScopeRequest.js | 37 ++++ src/resolver/Resolver.js | 193 +++++++++--------- .../aggregation/dsrAggregationLimit.json | 63 ++++++ test/unit/ScopeRequest.test.js | 113 +++++++++- test/unit/resolver/Resolver.test.js | 37 ++++ 5 files changed, 346 insertions(+), 97 deletions(-) create mode 100644 test/fixtures/aggregation/dsrAggregationLimit.json diff --git a/src/ScopeRequest.js b/src/ScopeRequest.js index df0ce23..76de20b 100644 --- a/src/ScopeRequest.js +++ b/src/ScopeRequest.js @@ -29,6 +29,15 @@ const VALID_OPERATORS = [ '$exists', ]; +const VALID_AGGREGATORS = [ + '$limit', + '$max', + '$min', + '$last', + '$first', + '$sort', +]; + const isLocal = url => (url.match('(http://|https://)?(localhost|127.0.0.*)') !== null); const isValidEvidenceChannelDetails = (channelDetails) => { @@ -102,6 +111,28 @@ class ScopeRequest { return true; } + /** + * Validate the constraints of an Scope Request + * @param filter of an aggregation in the Scope Request + * @returns {boolean} true|false + */ + static validateAggregationFilter(filter) { + const operatorKeys = _.keys(filter); + + if (operatorKeys.length !== 1) { + throw new Error('Invalid Constraint Object - only one operator is allowed'); + } + if (!_.includes(VALID_AGGREGATORS, operatorKeys[0])) { + throw new Error(`Invalid Aggregate Object - ${operatorKeys[0]} is not a valid filter`); + } + + if (_.isNil(filter[operatorKeys[0]])) { + throw new Error('Invalid Constraint Object - a constraint value is required'); + } + + return true; + } + /** * Check o credential commons if it is an valid global identifier * @param identifier @@ -181,6 +212,12 @@ class ScopeRequest { }); } } + + if (!_.isEmpty(item.aggregate)) { + _.forEach(item.aggregate, (aggregationFilter) => { + ScopeRequest.validateAggregationFilter(aggregationFilter); + }); + } } }); return true; diff --git a/src/resolver/Resolver.js b/src/resolver/Resolver.js index a467424..da9c598 100644 --- a/src/resolver/Resolver.js +++ b/src/resolver/Resolver.js @@ -34,117 +34,120 @@ function DsrResolver() { this.filterCredentials = (scope, credentials) => { const filtered = []; scope.credentialItems.forEach((credentialItem) => { - // the path of the scope is an multi value array it can be either an string (direct GLOBAL identifier) or an object with - // the identifier, we need to check on the type array of an VC if the type of the global identifier matches - const globalIdentifier = typeof credentialItem === 'string' ? credentialItem : credentialItem.identifier; - // the type of the VC eg civ:Type:address or cvc:Identity:name - const globalIdentifierType = globalIdentifier.substring(0, globalIdentifier.indexOf('-')); + // if the DSR only contains aggregate tag, ignore this part, if it contains both, first filter by constraints tag + if (credentialItem.constraints || !credentialItem.aggregate) { + // the path of the scope is an multi value array it can be either an string (direct GLOBAL identifier) or an object with + // the identifier, we need to check on the type array of an VC if the type of the global identifier matches + const globalIdentifier = typeof credentialItem === 'string' ? credentialItem : credentialItem.identifier; + // the type of the VC eg civ:Type:address or cvc:Identity:name + const globalIdentifierType = globalIdentifier.substring(0, globalIdentifier.indexOf('-')); - // for credentials we filter out the credentials, the meta issuer than the claim path - if (globalIdentifierType === 'credential') { - const type = globalIdentifier.substring('credential-'.length, globalIdentifier.lastIndexOf('-')); - // filter the VCs, do not confuse this $eq with the operator $eq on credentialItems array - const tempFiltered = credentials.filter(sift({ identifier: { $regex: `${type}` } })); + // for credentials we filter out the credentials, the meta issuer than the claim path + if (globalIdentifierType === 'credential') { + const type = globalIdentifier.substring('credential-'.length, globalIdentifier.lastIndexOf('-')); + // filter the VCs, do not confuse this $eq with the operator $eq on credentialItems array + const tempFiltered = credentials.filter(sift({ identifier: { $regex: `${type}` } })); - // if there is an constraint on the credential - if (credentialItem.identifier) { - const filterArgArray = []; - // filtering meta constraints if they exist - if (credentialItem.constraints.meta) { - if (credentialItem.constraints.meta.issued) { - // there is only one key - const operatorIssued = Object.keys(credentialItem.constraints.meta.issued.is)[0]; - const convertedOperatorIssued = this.convertMongoOperatorToJavascript(Object.keys(credentialItem.constraints.meta.issued.is)[0]); - const claimConstraintIssued = credentialItem.constraints.meta.issued.is[operatorIssued]; - const claimFilterIssued = {}; - claimFilterIssued.$where = `new Date(this.issued).getTime() ${convertedOperatorIssued} ${claimConstraintIssued}`; - filterArgArray.push(claimFilterIssued); - } - if (credentialItem.constraints.meta.expiry) { - // there is only one key - const operatorExpiry = Object.keys(credentialItem.constraints.meta.expiry.is)[0]; - const convertedOperatorExpiry = this.convertMongoOperatorToJavascript(Object.keys(credentialItem.constraints.meta.expiry.is)[0]); - const claimConstraintExpiry = credentialItem.constraints.meta.expiry.is[operatorExpiry]; - const claimFilterExpiry = {}; - claimFilterExpiry.$where = `new Date(this.expiry).getTime() ${convertedOperatorExpiry} ${claimConstraintExpiry}`; - filterArgArray.push(claimFilterExpiry); + // if there is an constraint on the credential + if (credentialItem.identifier) { + const filterArgArray = []; + // filtering meta constraints if they exist + if (credentialItem.constraints.meta) { + if (credentialItem.constraints.meta.issued) { + // there is only one key + const operatorIssued = Object.keys(credentialItem.constraints.meta.issued.is)[0]; + const convertedOperatorIssued = this.convertMongoOperatorToJavascript(Object.keys(credentialItem.constraints.meta.issued.is)[0]); + const claimConstraintIssued = credentialItem.constraints.meta.issued.is[operatorIssued]; + const claimFilterIssued = {}; + claimFilterIssued.$where = `new Date(this.issued).getTime() ${convertedOperatorIssued} ${claimConstraintIssued}`; + filterArgArray.push(claimFilterIssued); + } + if (credentialItem.constraints.meta.expiry) { + // there is only one key + const operatorExpiry = Object.keys(credentialItem.constraints.meta.expiry.is)[0]; + const convertedOperatorExpiry = this.convertMongoOperatorToJavascript(Object.keys(credentialItem.constraints.meta.expiry.is)[0]); + const claimConstraintExpiry = credentialItem.constraints.meta.expiry.is[operatorExpiry]; + const claimFilterExpiry = {}; + claimFilterExpiry.$where = `new Date(this.expiry).getTime() ${convertedOperatorExpiry} ${claimConstraintExpiry}`; + filterArgArray.push(claimFilterExpiry); + } + if (credentialItem.constraints.meta.issuer) { + const claimPathIssuer = 'issuer'; + // there is only one key + const operatorIssuer = Object.keys(credentialItem.constraints.meta.issuer.is)[0]; + const claimConstraintIssuer = credentialItem.constraints.meta.issuer.is[operatorIssuer]; + const claimFilterIssuer = {}; + claimFilterIssuer[claimPathIssuer] = claimConstraintIssuer; + filterArgArray.push(claimFilterIssuer); + } } - if (credentialItem.constraints.meta.issuer) { - const claimPathIssuer = 'issuer'; - // there is only one key - const operatorIssuer = Object.keys(credentialItem.constraints.meta.issuer.is)[0]; - const claimConstraintIssuer = credentialItem.constraints.meta.issuer.is[operatorIssuer]; - const claimFilterIssuer = {}; - claimFilterIssuer[claimPathIssuer] = claimConstraintIssuer; - filterArgArray.push(claimFilterIssuer); + + // this is the structure on the dsr { "path": "claim.path", "is": {"operator": "valueToFilter"} }, + // for each constraint, we have to filter out the credentials + if (credentialItem.constraints && credentialItem.constraints.claims) { + credentialItem.constraints.claims.forEach((claim) => { + const claimPath = `claim.${claim.path}`; + // there is only one key + const operator = Object.keys(claim.is)[0]; + const claimConstraint = claim.is[operator]; + const claimFilter = {}; + claimFilter[claimPath] = claimConstraint; + filterArgArray.push(claimFilter); + }); } + // with all the filters, do one query + const filterArg = { $and: filterArgArray }; + filtered.push(...tempFiltered.filter(sift(filterArg))); + } else { + filtered.push(...tempFiltered); } + } else if (globalIdentifierType === 'claim') { + // for UCAs it can either be a type, or an alsoKnown as + const type = globalIdentifier.substring('claim-'.length, globalIdentifier.lastIndexOf('-')); + const definition = ucaDefinitions.find(def => def.identifier === type); + const tempFiltered = []; + const filterArgArray = []; - // this is the structure on the dsr { "path": "claim.path", "is": {"operator": "valueToFilter"} }, - // for each constraint, we have to filter out the credentials - if (credentialItem.constraints && credentialItem.constraints.claims) { - credentialItem.constraints.claims.forEach((claim) => { - const claimPath = `claim.${claim.path}`; - // there is only one key - const operator = Object.keys(claim.is)[0]; - const claimConstraint = claim.is[operator]; + // if the definition has alsoKnown, the identifier and the path we should be looking is the aka + if (definition.alsoKnown) { + definition.alsoKnown.forEach((identifier) => { + const ucaType = identifier.substring(identifier.indexOf(':') + 1, identifier.lastIndexOf(':')).toLowerCase(); + const propertyPath = identifier.substring(identifier.lastIndexOf(':') + 1); const claimFilter = {}; - claimFilter[claimPath] = claimConstraint; - filterArgArray.push(claimFilter); + // if it is a directly global identifier, return any VC that the claim path has this property + claimFilter[`claim.${ucaType}.${propertyPath}`] = { $exists: true }; + tempFiltered.push(...credentials.filter(sift(claimFilter))); }); - } - // with all the filters, do one query - const filterArg = { $and: filterArgArray }; - filtered.push(...tempFiltered.filter(sift(filterArg))); - } else { - filtered.push(...tempFiltered); - } - } else if (globalIdentifierType === 'claim') { - // for UCAs it can either be a type, or an alsoKnown as - const type = globalIdentifier.substring('claim-'.length, globalIdentifier.lastIndexOf('-')); - const definition = ucaDefinitions.find(def => def.identifier === type); - const tempFiltered = []; - const filterArgArray = []; - - // if the definition has alsoKnown, the identifier and the path we should be looking is the aka - if (definition.alsoKnown) { - definition.alsoKnown.forEach((identifier) => { + } else { + const { identifier } = definition; const ucaType = identifier.substring(identifier.indexOf(':') + 1, identifier.lastIndexOf(':')).toLowerCase(); const propertyPath = identifier.substring(identifier.lastIndexOf(':') + 1); const claimFilter = {}; // if it is a directly global identifier, return any VC that the claim path has this property claimFilter[`claim.${ucaType}.${propertyPath}`] = { $exists: true }; tempFiltered.push(...credentials.filter(sift(claimFilter))); - }); - } else { - const { identifier } = definition; - const ucaType = identifier.substring(identifier.indexOf(':') + 1, identifier.lastIndexOf(':')).toLowerCase(); - const propertyPath = identifier.substring(identifier.lastIndexOf(':') + 1); - const claimFilter = {}; - // if it is a directly global identifier, return any VC that the claim path has this property - claimFilter[`claim.${ucaType}.${propertyPath}`] = { $exists: true }; - tempFiltered.push(...credentials.filter(sift(claimFilter))); - } + } - // if we are not a simple string - if (credentialItem.identifier) { - const ucaType = credentialItem.identifier.substring(credentialItem.identifier.indexOf(':') + 1, credentialItem.identifier.lastIndexOf(':')).toLowerCase(); - // iterate all over our credentials - if (credentialItem.constraints && credentialItem.constraints.claims) { - credentialItem.constraints.claims.forEach((claim) => { - const claimPath = `claim.${ucaType}.${claim.path}`; - // there is only one key - const operator = Object.keys(claim.is)[0]; - const claimConstraint = claim.is[operator]; - const constraintFilter = {}; - constraintFilter[claimPath] = claimConstraint; - filterArgArray.push(constraintFilter); - }); + // if we are not a simple string + if (credentialItem.identifier) { + const ucaType = credentialItem.identifier.substring(credentialItem.identifier.indexOf(':') + 1, credentialItem.identifier.lastIndexOf(':')).toLowerCase(); + // iterate all over our credentials + if (credentialItem.constraints && credentialItem.constraints.claims) { + credentialItem.constraints.claims.forEach((claim) => { + const claimPath = `claim.${ucaType}.${claim.path}`; + // there is only one key + const operator = Object.keys(claim.is)[0]; + const claimConstraint = claim.is[operator]; + const constraintFilter = {}; + constraintFilter[claimPath] = claimConstraint; + filterArgArray.push(constraintFilter); + }); + } + const filterArg = { $and: filterArgArray }; + filtered.push(...tempFiltered.filter(sift(filterArg))); + } else { + filtered.push(...tempFiltered); } - const filterArg = { $and: filterArgArray }; - filtered.push(...tempFiltered.filter(sift(filterArg))); - } else { - filtered.push(...tempFiltered); } } }); diff --git a/test/fixtures/aggregation/dsrAggregationLimit.json b/test/fixtures/aggregation/dsrAggregationLimit.json new file mode 100644 index 0000000..52b5579 --- /dev/null +++ b/test/fixtures/aggregation/dsrAggregationLimit.json @@ -0,0 +1,63 @@ +{ + "version": "1", + "requesterInfo": { + "app": { + "id": "TestPartnerApp", + "name": "TestPartnerApp", + "logo": "https://s-media-cache-ak0.pinimg.com/originals.png", + "description": "TestPartnerApp", + "primaryColor": "A80B00", + "secondaryColor": "FFFFFF" + }, + "requesterId": "TestPartnerId" + }, + "timestamp": "2018-07-22T14:06:35.879Z", + "credentialItems": [ + { + "identifier": "credential-cvc:Identity-v1", + "constraints": { + "meta": { + "issuer": { + "is": { + "$eq": "jest:test:2d516330-d2cc-11e8-b214-99085237d65e" + } + }, + "issuanceDate": { + "is": { + "$gt": 1509999999999 + } + }, + "expirationDate": { + "is": { + "$lt": 1999999999999 + } + } + }, + "claims": [ + { + "path": "identity.name.familyNames", + "is": { + "$eq": "djNLf8eWmO" + } + }, + { + "path": "identity.name.givenNames", + "is": { + "$eq": "qigCmvByou" + } + } + ] + }, + "aggregate": [ + { + "$limit": 3 + } + ] + } + ], + "channels": { + "eventsURL": "https://localhost/sr/events/abcd", + "payloadURL": "https://localhost/sr/payload/abcd" + }, + "authorization": {} +} \ No newline at end of file diff --git a/test/unit/ScopeRequest.test.js b/test/unit/ScopeRequest.test.js index 2f38b8f..09ca79a 100644 --- a/test/unit/ScopeRequest.test.js +++ b/test/unit/ScopeRequest.test.js @@ -395,7 +395,7 @@ describe('DSR Factory Tests', () => { ['credential-cvc:Identity-v1'], validConfig.channels, validConfig.app, - validConfig.partner + validConfig.partner, ); expect(dsr.requesterInfo.requesterId).toBe(validConfig.partner.id); }); @@ -406,7 +406,7 @@ describe('DSR Factory Tests', () => { ['credential-cvc:Identity-v1'], validConfig.channels, validConfig.app, - validConfig.partner + validConfig.partner, ); expect(dsr).toBeDefined(); expect(dsr.authentication).toBeDefined(); @@ -893,6 +893,115 @@ describe('DSR Request Utils', () => { }); expect(dsr).toBeDefined(); }); + + it('Should throw an error when creating a DSR with wrong aggregate filters', () => { + const functionToThrow = () => { + // eslint-disable-next-line no-new + new ScopeRequest('abcd', [ + { + identifier: 'claim-cvc:Phone.countryCode-v1', + aggregate: [ + { + $somethingToError: [ + { + path: 'meta.issuer', + is: { + $eq: 'did:ethr:0x1a88a35421a4a0d3e13fe4e8ebcf18e9a249dc5a', + }, + }, + { + path: 'claims.contact.phoneNumber.countryCode', + is: { + $eq: '55', + }, + }, + ], + }, + ], + }]); + }; + expect(functionToThrow).toThrow('Invalid Aggregate Object - $somethingToError is not a valid filter'); + }); + + it('Should Construct DSR with aggregation first', () => { + const dsr = new ScopeRequest('abcd', [ + { + identifier: 'credential-cvc:Covid19-v1', + aggregate: [ + { + $first: 'true', + }, + ], + }]); + expect(dsr).toBeDefined(); + }); + + it('Should Construct DSR with aggregation last', () => { + const dsr = new ScopeRequest('abcd', [ + { + identifier: 'credential-cvc:Covid19-v1', + aggregate: [ + { + $last: 'true', + }, + ], + }]); + expect(dsr).toBeDefined(); + }); + + it('Should Construct DSR with aggregation limit', () => { + const dsr = new ScopeRequest('abcd', [ + { + identifier: 'credential-cvc:Covid19-v1', + aggregate: [ + { + $limit: 3, + }, + ], + }]); + expect(dsr).toBeDefined(); + }); + + it('Should Construct DSR with aggregation max', () => { + const dsr = new ScopeRequest('abcd', [ + { + identifier: 'credential-cvc:Covid19-v1', + aggregate: [ + { + $max: 'claims.medical.covid19.patient.dateOfBirth', + }, + ], + }]); + expect(dsr).toBeDefined(); + }); + + it('Should Construct DSR with aggregation min', () => { + const dsr = new ScopeRequest('abcd', [ + { + identifier: 'credential-cvc:Covid19-v1', + aggregate: [ + { + $min: 'claims.medical.covid19.patient.dateOfBirth', + }, + ], + }]); + expect(dsr).toBeDefined(); + }); + + it('Should Construct DSR with aggregation sort', () => { + const dsr = new ScopeRequest('abcd', [ + { + identifier: 'claim-cvc:Phone.countryCode-v1', + aggregate: [ + { + $sort: { + 'meta.issuanceDate': 'ASC', + }, + }, + ], + }]); + expect(dsr).toBeDefined(); + }); }); module.exports = ScopeRequest; diff --git a/test/unit/resolver/Resolver.test.js b/test/unit/resolver/Resolver.test.js index 728e56d..730244d 100644 --- a/test/unit/resolver/Resolver.test.js +++ b/test/unit/resolver/Resolver.test.js @@ -296,4 +296,41 @@ describe('DSR Filtering and Constraints Tests', () => { expect(filtered.length).toBe(0); done(); }); + + it('Should not give any errors on filtering DSR with only aggregate tags', async (done) => { + const dsr = new ScopeRequest('abcd', [ + { + identifier: 'credential-cvc:Covid19-v1', + aggregate: [ + { + $limit: 3, + }, + ], + }], config.channels, config.app, config.partner); + const resolver = new Resolver(); + const filtered = await resolver.filterCredentials(dsr, []); + expect(filtered.length).toBe(0); + done(); + }); + + it('Should filter only by constraints tag and ignore the aggregate tag', async (done) => { + const unresolvedFileContents = fs.readFileSync('test/fixtures/aggregation/dsrAggregationLimit.json', 'utf8'); + const unresolvedRequest = JSON.parse(unresolvedFileContents); + + const credential1FileContent = fs.readFileSync('test/fixtures/filtering/identity1.json', 'utf8'); + const credential1 = JSON.parse(credential1FileContent); + + const credential2FileContent = fs.readFileSync('test/fixtures/filtering/identity2.json', 'utf8'); + const credential2 = JSON.parse(credential2FileContent); + + const credential3FileContent = fs.readFileSync('test/fixtures/filtering/identity3.json', 'utf8'); + const credential3 = JSON.parse(credential3FileContent); + + const resolver = new Resolver(); + const filtered = await resolver.filterCredentials(unresolvedRequest, [credential1, credential2, credential3]); + expect(filtered.length).toBe(1); + const vc = filtered[0]; + expect(vc.issuer).toBe('jest:test:2d516330-d2cc-11e8-b214-99085237d65e'); + done(); + }); }); From 491bd2b6255e8c587019fef011c1bfd78d22d51b Mon Sep 17 00:00:00 2001 From: Daniel Augusto de Melo Santos Date: Wed, 17 Feb 2021 11:17:33 -0300 Subject: [PATCH 2/2] [CIV-2716] Supporting new aggregate tags on DSR --- src/ScopeRequest.js | 2 +- test/unit/ScopeRequest.test.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/ScopeRequest.js b/src/ScopeRequest.js index 76de20b..c47e7b6 100644 --- a/src/ScopeRequest.js +++ b/src/ScopeRequest.js @@ -113,7 +113,7 @@ class ScopeRequest { /** * Validate the constraints of an Scope Request - * @param filter of an aggregation in the Scope Request + * @param {Object} filter of an aggregation in the Scope Request * @returns {boolean} true|false */ static validateAggregationFilter(filter) { diff --git a/test/unit/ScopeRequest.test.js b/test/unit/ScopeRequest.test.js index 09ca79a..8cf4d96 100644 --- a/test/unit/ScopeRequest.test.js +++ b/test/unit/ScopeRequest.test.js @@ -1002,6 +1002,27 @@ describe('DSR Request Utils', () => { }]); expect(dsr).toBeDefined(); }); + + it('Should Construct DSR with multiple aggregation filters', () => { + const dsr = new ScopeRequest('abcd', [ + { + identifier: 'claim-cvc:Phone.countryCode-v1', + aggregate: [ + { + $max: 'claims.medical.covid19.patient.dateOfBirth', + }, + { + $sort: { + 'meta.issuanceDate': 'ASC', + }, + }, + { + $limit: 3, + }, + ], + }]); + expect(dsr).toBeDefined(); + }); }); module.exports = ScopeRequest;