@identity.com/dsr
Version:
The Dynamic Scope Request (DSR) javascript library provides capability around securely requesting credential information between an ID Requester and an ID Holder
215 lines (198 loc) • 10.2 kB
JavaScript
const sift = require('sift').default;
const { schemaLoader } = require('@identity.com/credential-commons');
const { VALIDATION_MODE } = require('../ScopeRequest');
/**
* Dynamic Scope Request resolver. Do the filtering of credentials by passing unresolved scope requests
*
* @returns {*}
* @constructor
*/
function DsrResolver() {
this.convertMongoOperatorToJavascript = operator => {
if (operator === '$gt') {
return '>';
}
if (operator === '$gte') {
return '>=';
}
if (operator === '$lt') {
return '<';
}
if (operator === '$lte') {
return '<=';
}
return null;
};
this.filterCredentialsByIdentifier = (globalIdentifier, credentials, credentialItem, scope, filtered) => {
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 (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 (scope.mode && scope.mode === VALIDATION_MODE.ADVANCED) {
// 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.credentials.push(...tempFiltered.filter(sift(filterArg)));
} else 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);
const filterArgFailed = { $nor: filterArgArray };
filtered.failedConstraints.push(...tempFiltered.filter(sift(filterArgFailed)));
const filterArgValid = { $and: filterArgArray, $nin: filtered.failedConstraints };
filtered.credentials.push(...tempFiltered.filter(sift(filterArgValid)));
});
} else {
filtered.credentials.push(...tempFiltered);
}
} else {
filtered.credentials.push(...tempFiltered);
}
};
this.filterCredentialsByClaim = (globalIdentifier, credentials, credentialItem, scope, filtered) => {
// for UCAs it can either be a type, or an alsoKnown as
const type = globalIdentifier.substring('claim-'.length, globalIdentifier.lastIndexOf('-'));
const definition = schemaLoader.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 => {
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) {
if (scope.mode && scope.mode === VALIDATION_MODE.ADVANCED) {
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.credentials.push(...tempFiltered.filter(sift(filterArg)));
} else {
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 filterArgFailed = { $nor: filterArgArray };
filtered.failedConstraints.push(...tempFiltered.filter(sift(filterArgFailed)));
const filterArgValid = { $and: filterArgArray, $nin: filtered.failedConstraints };
filtered.credentials.push(...tempFiltered.filter(sift(filterArgValid)));
});
}
} else {
filtered.credentials.push(...tempFiltered);
}
} else {
filtered.credentials.push(...tempFiltered);
}
};
/**
*
* Resolve the set of required user data for verification
*
* @param scope the scope request and it's criteria for selection
* @param credentials a list of user credentials for filtering
*/
this.filterCredentials = (scope, credentials) => {
const filtered = {
credentials: [],
failedConstraints: []
};
// eslint-disable-next-line no-restricted-syntax
for (const credentialItem of scope.credentialItems) {
// 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') {
this.filterCredentialsByIdentifier(globalIdentifier, credentials, credentialItem, scope, filtered);
} else if (globalIdentifierType === 'claim') {
this.filterCredentialsByClaim(globalIdentifier, credentials, credentialItem, scope, filtered);
}
}
}
return filtered;
};
return this;
}
module.exports = DsrResolver;