Skip to content

Commit

Permalink
Merge pull request #33 from identity-com/feature/CIV-2716_agreggations
Browse files Browse the repository at this point in the history
[CIV-2716] Supporting new aggregate tags on DSR
  • Loading branch information
dmelosantos authored Feb 17, 2021
2 parents 53d12e1 + 491bd2b commit c32eae9
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 97 deletions.
37 changes: 37 additions & 0 deletions src/ScopeRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -102,6 +111,28 @@ class ScopeRequest {
return true;
}

/**
* Validate the constraints of an Scope Request
* @param {Object} 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
Expand Down Expand Up @@ -181,6 +212,12 @@ class ScopeRequest {
});
}
}

if (!_.isEmpty(item.aggregate)) {
_.forEach(item.aggregate, (aggregationFilter) => {
ScopeRequest.validateAggregationFilter(aggregationFilter);
});
}
}
});
return true;
Expand Down
193 changes: 98 additions & 95 deletions src/resolver/Resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});
Expand Down
63 changes: 63 additions & 0 deletions test/fixtures/aggregation/dsrAggregationLimit.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
Loading

0 comments on commit c32eae9

Please sign in to comment.