// Copyright contributors to the IBM Security Verify Privacy SDK
// for JavaScript project
const ConfigurationError = require('./errors/configurationError');
const DPCMService = require('./services/dpcm/dpcmService');
const StringUtils = require('./utils/stringUtils');
const debug = require('debug')('verify:privacy');
/**
* Privacy module is the main module in the Privacy SDK
* @module Privacy
*/
/**
* Class representing the Privacy SDK for IBM Security Verify. Used to
* perform privacy assessment for attributes being requested and metadata
* required to build consent experiences.
*
* @author Vivek Shankar
*/
class Privacy {
/**
* Create a new {@link Privacy} object.
*
* @param {Object} config Global configuration for the SDK
* @param {string} config.tenantUrl The Verify tenant hostname, including
* the protocol.
* @param {Object} auth Auth object contains property values to authorize
* requests to Verify
* @param {string} auth.accessToken The OAuth 2.0 token used to authorize
* requests. If the access token is generated using a privileged API client
* (as opposed to one generated on a user authentication flow), the
* <code>context.subjectId</code> is required.
* @param {Object} context Context object contains Privacy SDK specific
* context
* @param {string} context.subjectId The user/subject identifier that may be
* a Verify user identifier.
* @param {boolean} context.isExternalSubject Indicates if the subject is
* known to Verify.
* @param {string} context.ipAddress The IP address of the user agent. If this
* library is used in a backend system, this IP should be obtained from the
* request headers that contain the actual user agent IP address.
*
* @example
* const Privacy = require('verify-privacy-sdk-js');
* const client = new Privacy({
* "tenantUrl": "https://abc.verify.ibm.com"
* }, {
* "accessToken": "lasfjsdlfjsldjfglsdjfglsjl"
* }, {
* "ipAddress": "1.2.3.4"
* });
*/
constructor(config, auth, context = {}) {
if (!StringUtils.has(config, 'tenantUrl')) {
throw new ConfigurationError(
`Cannot find property 'tenantUrl' in configuration settings.`);
}
if (!StringUtils.has(auth, 'accessToken')) {
throw new ConfigurationError(
`Cannot find property 'accessToken' in auth`);
}
this._config = config;
this._auth = auth;
this._context = context;
}
/**
* Evaluate the attributes requested for approval.
*
* Request the consent management system to approve the use of attributes
* for the specified purpose, access type and an optional value. If the
* access type is not specified, it is set to a system default.
*
* @param {Array} items The data items that require approval for use
* @param {string} items.purposeId The purpose ID representing the privacy
* purpose configured on Verify. If you are checking for the consent status
* of EULA, use the EULA identifier here.
* @param {string} items.profileId The Privacy profile ID configured on
* Verify. If provided, other fields are ignored and assessment is performed
* using this identifier.
* @param {string} items.accessTypeId The access type ID representing the
* available access types on Verify. This must be one of the access types
* selected for the purpose.
* @param {string} items.attributeId The attribute ID on Verify. This must be
* configured as one of the attributes for the purpose. This may be optional
* if no attributes are configured for the purpose. If this is empty and the
* purpose has associated attributes, all attributes are assessed and the
* decision is included in the result array.
* @param {string} items.attributeValue The attribute value for the attribute.
* This is typically used when the user has more than one value for the
* attribute. This is optional.
*
* @return {Promise<WrappedAssessment>} The status of the assessment
* and additional details
*
* @example
* let r = await client.assess([
* {
* // allow mobile number for marketing
* "purposeId": "marketing",
* "attributeId": "mobile_number",
* "accessTypeId": "default"
* },
* {
* // default end user license agreement
* "purposeId": "defaultEULA",
* },
* {
* // Privacy profile identifier
* "profileId": "gdprprofile",
* }
* ])
*
* if (r.status == "consent") {
* // redirect for consent or build the page here
* // and render. consider filtering out items
* // in the assessment that are not approved because
* // of a rule violation
* } else if (r.status == "approved") {
* // the world is your oyster. go forth and conquer
* } else {
* // examine the assessment and show an appropriate error
* }
*/
async assess(items) {
const methodName = `${Privacy.name}:assess(items)`;
const service = new DPCMService(
this._auth, this._config.tenantUrl, this._context);
try {
const assessment = await service.requestApproval(items);
debug(`[${methodName}]`, 'assessment:',
JSON.stringify(assessment));
// process the response
if (!Array.isArray(assessment)) {
const desc = 'assessment is expected to be an array. Received ' +
`${typeof assessment}`;
return {
status: 'error',
error: {
'messageId': 'INVALID_DATATYPE',
'messageDescription': desc,
},
};
}
let status = await service.processAssessment(assessment);
if (status == null) {
// final fallback. shouldn't happen
status = 'denied';
}
return {
status: status,
assessment,
};
} catch (error) {
const jsonResp = {status: 'error'};
if (error.response.data) {
jsonResp.error = error.response.data;
debug(`[${methodName}]`, 'error data:', error.response.data);
} else {
debug(`[${methodName}]`, 'error:', error);
}
return jsonResp;
}
}
/**
* Get consent metadata that can be used to build the consent page presented
* to the data subject/user, including the current state of consent.
*
* @param {Array} items The data items that require approval for use
* @param {string} items.purposeId The purpose ID representing the privacy
* purpose configured on Verify. If you are checking for the consent status
* of EULA, use the EULA identifier here.
* @param {string} items.accessTypeId The access type ID representing the
* available access types on Verify. This must be one of the access types
* selected for the purpose. If this is not provided in the input, it is
* defaulted to 'default'. Wildcards are not allowed.
* @param {string} items.attributeId The attribute ID on Verify. This must be
* configured as one of the attributes for the purpose. This may be optional
* if no attributes are configured for the purpose. Wildcards are not allowed.
* @param {string} items.attributeValue The attribute value for the attribute.
* This is typically used when the user has more than one value for the
* attribute. This is optional.
* @param {Object} headers Optional headers that can be sent
* @param {string} headers.Accept-Language The locale of content to
* receive in the response.
* @return {Promise<WrappedMetadata>} The status of the request
* and any consent metadata
*
* @example
* let r = await client.getConsentMetadata([
* {
* // allow mobile number for marketing
* "purposeId": "marketing",
* "attributeId": "mobile_number",
* "accessTypeId": "default"
* },
* {
* // default end user license agreement
* "purposeId": "defaultEULA",
* }
* ])
*
* if (r.status == "done") {
* // render the page based on the r.metadata
* }
*/
async getConsentMetadata(items, headers = {}) {
const methodName = `${Privacy.name}:getConsentMetadata(items)`;
const service = new DPCMService(this._auth, this._config.tenantUrl,
this._context, headers);
try {
// retrieve the list of purposes
const purposes = new Set();
const itemFilter = {};
for (const item of items) {
purposes.add(item.purposeId);
if (!item['accessTypeId'] || item['accessTypeId'] == null) {
item.accessTypeId = 'default';
}
const itemKey = item.purposeId + '/' +
StringUtils.getOrDefault(item, 'attributeId', '') +
'.' + StringUtils.getOrDefault(item, 'accessTypeId', '');
const attrValue = StringUtils.getOrDefault(item, 'attributeValue', '');
if (!itemFilter.hasOwnProperty(itemKey)) {
// initialize
itemFilter[itemKey] = [attrValue];
} else {
itemFilter[itemKey].push(attrValue);
}
}
// get metadata
const response = await service.getConsentMetadata(Array.from(purposes));
debug(`[${methodName}]`, 'response:', JSON.stringify(response));
// filter and normalize
const metadata = await service.processConsentMetadata(
itemFilter, response);
debug(`[${methodName}]`, 'metadata:', JSON.stringify(metadata));
return {status: 'done', metadata};
} catch (error) {
const jsonResp = {status: 'error'};
if (error.response.data) {
jsonResp.error = error.response.data;
debug(`[${methodName}]`, 'error data:', error.response.data);
} else {
debug(`[${methodName}]`, 'error:', error);
}
return jsonResp;
}
}
/**
* Fetches user consents.
*
* @param {Object} options An optional parameter object
* @param {boolean} options.filterByCurrentApplication If set to true,
* filters consentsby the application id present in the authentication token
* @return {Promise<WrappedGetUserConsents>}
*
* @example
* let r = await client.getUserConsents()
* if (r.status == "done") {
* // render the page based on the r.consents
* }
*/
async getUserConsents(options) {
const methodName = `${Privacy.name}:getUserConsents()`;
const service = new DPCMService(this._auth, this._config.tenantUrl,
this._context);
try {
const resp = await service.getUserConsents(options);
debug(`[${methodName}]`, 'response:',
resp);
return {status: 'done', consents: resp.consents};
} catch (error) {
const jsonResp = {status: 'error'};
if (error.response && error.response.data) {
jsonResp.error = error.response.data;
debug(`[${methodName}]`, 'error data:', error.response.data);
} else {
debug(`[${methodName}]`, 'error:', error);
}
return jsonResp;
}
}
/**
* Store consents for the user.
* <br><br>Consents may only be created typically, except if the consent
* end time needs to be updated. Only 10 consent operations are allowed
* at a time.
*
* @param {Consent[]} consents The full consent records that need to be
* created or updated
*
* @return {Promise<WrappedStoreUserConsents>} Consent operation response
* @example
* let r = await client.storeConsents([
* {
* "purposeId": "marketing",
* "attributeId": "mobile_number",
* "state": 3 // opt-in
* }
* ])
*
* if (r.status == "success") {
* // Warp 11... engage
* } else {
* // loop through the r.results to determine what failed and why
* }
*/
async storeConsents(consents) {
const methodName = `${Privacy.name}:storeConsents(auth, consents)`;
if (!Array.isArray(consents)) {
const desc = 'consents are expected to be an array. Received ' +
`${typeof consents}`;
return {
status: 'error',
error: {
'messageId': 'INVALID_DATATYPE',
'messageDescription': desc,
},
};
}
const service = new DPCMService(this._auth, this._config.tenantUrl,
this._context);
try {
const r = await service.storeConsents(consents);
debug(`[${methodName}]`, 'response:',
r);
// parse the response
const status = (r.messageId != 'CSIBT0070I') ? 'fail' : 'success';
return {status: status, results: r.results};
} catch (error) {
debug(`[${methodName}]`, 'error:', error);
const jsonResp = {status: 'deny'};
if (error.response.data) {
jsonResp.error = error.response.data;
}
return jsonResp;
}
}
}
/**
* Enumeration of different possible consent display types
* @enum {ConsentDisplayTypesEnum}
* @readonly
*/
Privacy.ConsentDisplayTypes = {
DO_NOT_SHOW: 1,
TRANSPARENT: 2,
OPTIN_OR_OUT: 3,
ALLOW_OR_DENY: 4,
};
/**
* Enumeration of different possible consent types
* @enum {ConsentTypesEnum}
* @readonly
*/
Privacy.ConsentTypes = {
ALLOW: 1,
DENY: 2,
OPTIN: 3,
OPTOUT: 4,
TRANSPARENT: 5,
};
module.exports = Privacy;