UNPKG

@keepsolutions/scimgateway

Version:

Using SCIM protocol as a gateway for user provisioning to other endpoints

904 lines (820 loc) 37 kB
// ================================================================================= // File: plugin-restful.js // // Author: Jarle Elshaug // // Purpose: REST Webservice user-provisioning using REST endpoint "loki" // // Prereq: plugin-loki is up and running // // Supported attributes: // // GlobalUser Template Scim Endpoint // ----------------------------------------------------------------------------------------------- // User name %AC% userName userName // Suspended - active active // Password %P% password password // First Name %UF% name.givenName name.givenName // Last Name %UL% name.familyName name.familyName // Full Name %UN% name.formatted name.formatted // Job title %UT% title title // Email %UE% (Emails, type=Work) emails.work emails [type eq work] // Phone %UP% (Phone Numbers, type=Work) phoneNumbers.work phoneNumbers [type eq work] // Company %UCOMP% (Entitlements, type=Company) entitlements.company entitlements [type eq company] // // ================================================================================= 'use strict' const http = require('http') const https = require('https') const HttpsProxyAgent = require('https-proxy-agent') const URL = require('url').URL const querystring = require('querystring') // mandatory plugin initialization - start const path = require('path') let ScimGateway = null try { ScimGateway = require('scimgateway') } catch (err) { ScimGateway = require('./scimgateway') } const scimgateway = new ScimGateway() const pluginName = path.basename(__filename, '.js') const configDir = path.join(__dirname, '..', 'config') const configFile = path.join(`${configDir}`, `${pluginName}.json`) const validScimAttr = [ // array containing scim attributes supported by our plugin code. Empty array - all attrbutes are supported by endpoint 'userName', // userName or externalId is mandatory 'active', // active is mandatory for IM 'password', 'name.givenName', 'name.familyName', 'name.formatted', 'title', // "emails", // accepts all multivalues for this key 'emails.work', // accepts multivalues if type value equal work (lowercase) // "phoneNumbers", 'phoneNumbers.work', // "entitlements" 'entitlements.company' ] let config = require(configFile).endpoint config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file // mandatory plugin initialization - end const _serviceClient = {} // ================================================= // exploreUsers // ================================================= scimgateway.exploreUsers = async (baseEntity, attributes, startIndex, count) => { const action = 'exploreUsers' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" attributes=${attributes} startIndex=${startIndex} count=${count}`) const ret = { // itemsPerPage will be set by scimgateway Resources: [], totalResults: null } const method = 'GET' const path = `/Users${(attributes ? '?attributes=' + attributes : '')}` const body = null try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw (err) } else if (!response.body.Resources) { const err = new Error(`${action}: Got empty response on REST request`) throw (err) } if (!startIndex && !count) { // client request without paging startIndex = 1 count = response.body.Resources.length } const arrAttr = attributes.split(',') for (let index = startIndex - 1; index < response.body.Resources.length && (index + 1 - startIndex) < count; ++index) { const retObj = response.body.Resources[index] if (!attributes) ret.Resources.push(retObj) else { // return according to attributes (userName or externalId should normally be included and id=userName/externalId) let found = false const obj = {} for (let i = 0; i < arrAttr.length; i++) { const key = arrAttr[i].split('.')[0] // title => title, name.familyName => name if (retObj[key]) { obj[key] = retObj[key] found = true } } if (found) ret.Resources.push(obj) } } // not needed if client or endpoint do not support paging ret.totalResults = response.body.Resources.length ret.startIndex = startIndex return ret // all explored users } catch (err) { const newErr = err throw newErr } } // ================================================= // exploreGroups // ================================================= scimgateway.exploreGroups = async (baseEntity, attributes, startIndex, count) => { const action = 'exploreGroups' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" attributes=${attributes} startIndex=${startIndex} count=${count}`) const arrAttr = attributes.split(',') const ret = { // itemsPerPage will be set by scimgateway Resources: [], totalResults: null } const method = 'GET' const path = '/Groups' const body = null try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw err } else if (!response.body.Resources) { const err = new Error(`${action}: Got empty response on REST request`) throw err } if (!startIndex && !count) { // client request without paging startIndex = 1 count = response.body.Resources.length } for (let index = startIndex - 1; index < response.body.Resources.length && (index + 1 - startIndex) < count; ++index) { if (response.body.Resources[index].id && response.body.Resources[index].displayName) { const scimGroup = { // displayName and id is mandatory, note: we set id=displayName displayName: response.body.Resources[index].displayName, id: response.body.Resources[index].id, externalId: response.body.Resources[index].displayName } if (!attributes || (arrAttr.includes('members') || arrAttr.includes('members.value'))) { scimGroup.members = response.body.Resources[index].members } ret.Resources.push(scimGroup) } } // not needed if client or endpoint do not support paging ret.totalResults = response.body.Resources.length ret.startIndex = startIndex return ret // all explored users } catch (err) { const newErr = err throw newErr } } // ================================================= // getUser // ================================================= scimgateway.getUser = async (baseEntity, getObj, attributes) => { // getObj = { filter: <filterAttribute>, identifier: <identifier> } // e.g: getObj = { filter: 'userName', identifier: 'bjensen'} // filter: userName and id must be supported // (they are most often considered as "the same" where identifier = UserID ) // Note, the value of id attribute returned will be used by modifyUser and deleteUser // attributes: if not blank, attributes listed should be returned // Should normally return all supported user attributes having id and userName as mandatory // SCIM Gateway will automatically filter response according to the attributes list const action = 'getUser' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" ${getObj.filter}=${getObj.identifier} attributes=${attributes}`) /* const findObj = {} findObj[getObj.filter] = getObj.identifier // { userName: 'bjensen } / { externalId: 'bjensen } / { id: 'bjensen } / { 'emails.value': 'jsmith@example.com'} / { 'phoneNumbers.value': '555-555-5555'} */ const method = 'GET' let path if (getObj.filter === 'id') path = `/Users/${getObj.identifier}?attributes=${attributes}` // GET /Users/bjensen?attributes= else path = `/Users?filter=${getObj.filter} eq "${getObj.identifier}"${(attributes) ? '&attributes=' + attributes : ''}` // GET /Users?filter=userName eq "bjensen"&attributes=userName,active,name.givenName,name.familyName,name.formatted,title,emails,phoneNumbers,entitlements const body = null try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw err } else if (!response.body) { const err = new Error(`${action}: Got empty response on REST request`) throw err } let userObj if (response.body.Resources && Array.isArray(response.body.Resources) && response.body.Resources.length === 1) userObj = response.body.Resources[0] else userObj = response.body if (!userObj || Object.keys(userObj).length < 1) return null // user not found if (!userObj.name) userObj.name = {} if (!userObj.emails) userObj.emails = [{}] if (!userObj.phoneNumbers) userObj.phoneNumbers = [{}] if (!userObj.entitlements) userObj.entitlements = [{}] const objWorkEmail = scimgateway.getArrayObject(userObj, 'emails', 'work') const objWorkPhone = scimgateway.getArrayObject(userObj, 'phoneNumbers', 'work') const objCompanyEntitlement = scimgateway.getArrayObject(userObj, 'entitlements', 'company') let arrEmail = [] let arrPhone = [] let arrEntitlement = [] if (objWorkEmail) arrEmail.push(objWorkEmail) else arrEmail = null if (objWorkPhone) arrPhone.push(objWorkPhone) else arrPhone = null if (objCompanyEntitlement) arrEntitlement.push(objCompanyEntitlement) else arrEntitlement = null const retObj = { userName: userObj.userName, id: userObj.id, active: userObj.active, name: { givenName: userObj.name.givenName || '', familyName: userObj.name.familyName || '', formatted: userObj.name.formatted || '' }, title: userObj.title, emails: arrEmail, phoneNumbers: arrPhone, entitlements: arrEntitlement } // scimgateway will auto include groups if not included by plugin // in this use case it's already done when endpoint is scimgateway (plugin-loki) // groups can be retrieved using: await scimgateway.getGroupMembers(baseEntity, userObj.id, 'members.value,id,displayName') if (userObj.groups && Array.isArray(userObj.groups)) retObj.groups = userObj.groups if (!attributes) return retObj // user with all attributes // return according to attributes const ret = {} const arrAttr = attributes.split(',') for (let i = 0; i < arrAttr.length; i++) { const attr = arrAttr[i].split('.') // title / name.familyName / emails.value if (retObj[attr[0]]) { if (attr.length === 1) ret[attr[0]] = retObj[attr[0]] else if (retObj[attr[0]][attr[1]]) { // name.familyName if (!ret[attr[0]]) ret[attr[0]] = {} ret[attr[0]][attr[1]] = retObj[attr[0]][attr[1]] } else if (Array.isArray(retObj[attr[0]])) { // emails.value / phoneNumbers.type if (!ret[attr[0]]) ret[attr[0]] = [] const arr = retObj[attr[0]] for (let j = 0; j < arr.length; j++) { if (arr[j][attr[1]]) { const index = ret[attr[0]].findIndex(el => (el.value && arr[j].value && el.value === arr[j].value)) let o if (index < 0) { o = {} if (arr[j].value) o.value = arr[j].value // new, always include value } else o = ret[attr[0]][index] // existing o[attr[1]] = arr[j][attr[1]] if (index < 0) ret[attr[0]].push(o) else ret[attr[0]][index] = o } } } } } if (JSON.stringify(ret) === '{}') return retObj // user with all attributes when specified attributes not found return ret } catch (err) { const newErr = err throw newErr } } // ================================================= // createUser // ================================================= scimgateway.createUser = async (baseEntity, userObj) => { const action = 'createUser' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" userObj=${JSON.stringify(userObj)}`) const notValid = scimgateway.notValidAttributes(userObj, validScimAttr) if (notValid) { const err = new Error(`unsupported scim attributes: ${notValid} ` + `(supporting only these attributes: ${validScimAttr.toString()})` ) throw err } if (!userObj.name) userObj.name = {} if (!userObj.emails) userObj.emails = { work: {} } if (!userObj.phoneNumbers) userObj.phoneNumbers = { work: {} } if (!userObj.entitlements) userObj.entitlements = { company: {} } const arrEmail = [] const arrPhone = [] const arrEntitlement = [] if (userObj.emails.work.value) arrEmail.push(userObj.emails.work) if (userObj.phoneNumbers.work.value) arrPhone.push(userObj.phoneNumbers.work) if (userObj.entitlements.company.value) arrEntitlement.push(userObj.entitlements.company) const method = 'POST' const path = '/Users' const body = { userName: userObj.userName, active: userObj.active || true, password: userObj.password || null, name: { givenName: userObj.name.givenName || null, familyName: userObj.name.familyName || null, formatted: userObj.name.formatted || null }, title: userObj.title || '', emails: (arrEmail.length > 0) ? arrEmail : null, phoneNumbers: (arrPhone.length > 0) ? arrPhone : null, entitlements: (arrEntitlement.length > 0) ? arrEntitlement : null } try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw err } return null } catch (err) { const newErr = err throw newErr } } // ================================================= // deleteUser // ================================================= scimgateway.deleteUser = async (baseEntity, id) => { const action = 'deleteUser' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`) const method = 'DELETE' const path = `/Users/${id}` const body = null try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw err } return null } catch (err) { const newErr = err throw newErr } } // ================================================= // modifyUser // ================================================= scimgateway.modifyUser = async (baseEntity, id, attrObj) => { const action = 'modifyUser' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`) const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr) if (notValid) { const err = new Error(`unsupported scim attributes: ${notValid} ` + `(supporting only these attributes: ${validScimAttr.toString()})` ) throw err } if (!attrObj.name) attrObj.name = {} if (!attrObj.emails) attrObj.emails = {} if (!attrObj.phoneNumbers) attrObj.phoneNumbers = {} if (!attrObj.entitlements) attrObj.entitlements = {} const arrEmail = [] const arrPhone = [] const arrEntitlement = [] if (attrObj.emails.work) { if (!attrObj.emails.work.type) attrObj.emails.work.type = 'work' arrEmail.push(attrObj.emails.work) } if (attrObj.phoneNumbers.work) { if (!attrObj.phoneNumbers.work.type) attrObj.phoneNumbers.work.type = 'work' arrPhone.push(attrObj.phoneNumbers.work) } if (attrObj.entitlements.company) { if (!attrObj.entitlements.company.type) attrObj.entitlements.company.type = 'work' arrEntitlement.push(attrObj.entitlements.company) } const method = 'PATCH' const path = `/Users/${id}` let body = {} // { userName: id } if (attrObj.active === true) body.active = true else if (attrObj.active === false) body.active = false if (attrObj.password) body.password = attrObj.password if (attrObj.name.givenName || attrObj.name.givenName === '') { if (!body.name) body.name = {} body.name.givenName = attrObj.name.givenName } if (attrObj.name.familyName || attrObj.name.familyName === '') { if (!body.name) body.name = {} body.name.familyName = attrObj.name.familyName } if (attrObj.name.formatted || attrObj.name.formatted === '') { if (!body.name) body.name = {} body.name.formatted = attrObj.name.formatted } if (attrObj.title || attrObj.title === '') { body.title = attrObj.title } if (arrEmail.length > 0) { body.emails = arrEmail } if (arrPhone.length > 0) { body.phoneNumbers = arrPhone } if (arrEntitlement.length > 0) { body.entitlements = arrEntitlement } if (!config.entity[baseEntity].scimVersion || config.entity[baseEntity].scimVersion !== '1.1') { // scim 2.0 endpoint body = { Operations: [ { op: 'replace', value: body } ] } } try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw err } return null } catch (err) { const newErr = err throw newErr } } // ================================================= // getGroup // ================================================= scimgateway.getGroup = async (baseEntity, getObj, attributes) => { // getObj = { filter: <filterAttribute>, identifier: <identifier> } // e.g: getObj = { filter: 'displayName', identifier: 'GroupA' } // filter: displayName and id must be supported // (they are most often considered as "the same" where identifier = GroupName) // Note, the value of id attribute returned will be used by deleteGroup, getGroupMembers and modifyGroup // attributes: if not blank, attributes listed should be returned // Should normally return all supported group attributes having id, displayName and members as mandatory // members may be skipped if attributes is not blank and do not contain members or members.value const action = 'getGroup' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" ${getObj.filter}=${getObj.identifier} attributes=${attributes}`) const method = 'GET' const path = `/Groups?filter=${getObj.filter} eq "${getObj.identifier}"${(attributes) ? '&attributes=' + attributes : ''}` // GET = /Groups?filter=displayName eq "Admins"&attributes=externalId,id,members.value,displayName const body = null try { const response = await doRequest(baseEntity, method, path, body) if (!response.body.Resources) { const err = new Error(`${action}: Got empty response on REST request`) throw err } const retObj = {} if (response.body.Resources.length === 1) { const groupObj = response.body.Resources[0] if (!groupObj) return null // no group found // not parsing attributes in this example, returning what's mandatory for most IdP's retObj[getObj.filter] = groupObj[getObj.filter] // incase none of below (e.g. externalId) retObj.displayName = groupObj.displayName // mandatory retObj.id = groupObj.displayName // value same as displayName if (Array.isArray(groupObj.members)) { // comment out this line if using "users are member of group" retObj.members = [] groupObj.members.forEach((el) => { if (el.value) retObj.members.push({ value: el.value }) }) } } return retObj } catch (err) { const newErr = err throw newErr } } // ================================================= // getGroupMembers // ================================================= scimgateway.getGroupMembers = async (baseEntity, id, attributes) => { // return all groups the user is member of having attributes included e.g: members.value,id,displayName // method used when "users member of group", if used - getUser must treat user attribute groups as virtual readOnly attribute // "users member of group" is SCIM default and this method should normally have some logic const action = 'getGroupMembers' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" user id=${id} attributes=${attributes}`) const arrRet = [] const method = 'GET' const path = `/Groups?filter=members.value eq "${id}"&attributes=${attributes}` // GET = /Groups?filter=members.value eq "bjensen"&attributes=members.value,displayName const body = null try { const response = await doRequest(baseEntity, method, path, body) if (!response.body.Resources) { const err = new Error(`${action}: Got empty response on REST request`) throw err } response.body.Resources.forEach(function (element) { if (Array.isArray(element.members)) { element.members.forEach(function (el) { if (el.value === id) { // user is member of group const userGroup = { id: element.id, displayName: element.displayName, // displayName is mandatory members: [{ value: el.value }] // only includes current user } arrRet.push(userGroup) // { id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] } } }) } }) return arrRet } catch (err) { const newErr = err throw newErr } } // ================================================= // getGroupUsers // ================================================= scimgateway.getGroupUsers = async (baseEntity, id, attributes) => { // return array of all users that is member of this group id having attributes included e.g: groups.value,userName // method used when "group member of users", if used - getGroup must treat group attribute members as virtual readOnly attribute const action = 'getGroupUsers' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attributes=${attributes}`) const arrRet = [] return arrRet } // ================================================= // createGroup // ================================================= scimgateway.createGroup = async (baseEntity, groupObj) => { const action = 'createGroup' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" groupObj=${JSON.stringify(groupObj)}`) const method = 'POST' const path = '/Groups' const body = { displayName: groupObj.displayName } try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw err } return null } catch (err) { const newErr = err throw newErr } } // ================================================= // deleteGroup // ================================================= scimgateway.deleteGroup = async (baseEntity, id) => { const action = 'deleteGroup' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`) const method = 'DELETE' const path = `/Groups/${id}` const body = null try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw err } return null } catch (err) { const newErr = err throw newErr } } // ================================================= // modifyGroup // ================================================= scimgateway.modifyGroup = async (baseEntity, id, attrObj) => { const action = 'modifyGroup' scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`) if (!attrObj.members) { throw new Error(`plugin handling "${action}" only supports modification of members`) } if (!Array.isArray(attrObj.members)) { throw new Error(`plugin handling "${action}" error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`) } let body = {} if (config.entity[baseEntity].scimVersion && config.entity[baseEntity].scimVersion === '1.1') { // scim v1.1 endpoint body = { members: [] } attrObj.members.forEach(function (el) { if (el.operation && el.operation === 'delete') { // delete member from group // PATCH = /Groups/Admins Body = {"members":[{"operation":"delete","value":"bjensen"}]} body.members.push({ operation: 'delete', value: el.value }) } else { // add member to group/ // PATCH = /Groups/Admins Body = {"members":[{"value":"bjensen"}] body.members.push({ value: el.value }) } }) } else { // scim 2.0 endpoint const addValues = [] const removeValues = [] attrObj.members.forEach(function (el) { if (el.operation && el.operation === 'delete') { // delete member from group removeValues.push({ value: el.value }) } else { // add member to group/ addValues.push({ value: el.value }) } }) if (addValues.length < 1 && removeValues.length < 1) return null body = { Operations: [] } if (addValues.length > 0) { body.Operations.push( { op: 'add', path: 'members', value: addValues } ) } if (removeValues.length > 0) { body.Operations.push( { op: 'remove', path: 'members', value: removeValues } ) } } const method = 'PATCH' const path = `/Groups/${id}` try { const response = await doRequest(baseEntity, method, path, body) if (response.statusCode < 200 || response.statusCode > 299) { const err = new Error(`Error message: ${response.statusMessage} - ${JSON.stringify(response.body)}`) throw err } return null } catch (err) { const newErr = err throw newErr } } // ================================================= // helpers // ================================================= // // getServiceClient - returns options needed for connection parameters // // path = e.g. "/xxx/yyy", then using host/port/protocol based on config baseUrls[0] // auth automatically added and failover according to baseUrls array // // path = url e.g. "http(s)://<host>:<port>/xxx/yyy", then using the url host/port/protocol // opt (options) may be needed e.g {auth: {username: "username", password: "password"} } // const getServiceClient = async (baseEntity, method, path, opt) => { const action = 'getServiceClient' let urlObj if (!path) path = '' try { urlObj = new URL(path) } catch (err) { // // path (no url) - default approach and client will be cached based on config // if (_serviceClient[baseEntity]) { // serviceClient already exist scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using existing client`) } else { scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Client have to be created`) let client = null if (config.entity && config.entity[baseEntity]) client = config.entity[baseEntity] if (!client) { const err = new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`) throw err } urlObj = new URL(config.entity[baseEntity].baseUrls[0]) const param = { baseUrl: config.entity[baseEntity].baseUrls[0], options: { json: true, // json-object response instead of string headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + Buffer.from(`${config.entity[baseEntity].username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.password`, configFile)}`).toString('base64') }, host: urlObj.hostname, port: urlObj.port, // null if https and 443 defined in url protocol: urlObj.protocol, // http: or https: rejectUnauthorized: false // accepts self-siged certificates // 'method' and 'path' added at the end } } // proxy if (config.entity[baseEntity].proxy && config.entity[baseEntity].proxy.host) { const agent = new HttpsProxyAgent(config.entity[baseEntity].proxy.host) param.options.agent = agent // proxy if (config.entity[baseEntity].proxy.username && config.entity[baseEntity].proxy.password) { param.options.headers['Proxy-Authorization'] = 'Basic ' + Buffer.from(`${config.entity[baseEntity].proxy.username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.proxy.password`, configFile)}`).toString('base64') // using proxy with auth } } if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {} _serviceClient[baseEntity] = param // serviceClient created } const cli = scimgateway.copyObj(_serviceClient[baseEntity]) // client ready // failover support path = _serviceClient[baseEntity].baseUrl + path urlObj = new URL(path) cli.options.host = urlObj.hostname cli.options.port = urlObj.port cli.options.protocol = urlObj.protocol // adding none static cli.options.method = method cli.options.path = `${urlObj.pathname}${urlObj.search}` if (opt) cli.options = scimgateway.extendObj(cli.options, opt) // merge with argument options return cli // final client } // // url path - none config based and used as is (no cache) // scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using none config based client`) let options = { json: true, headers: { 'Content-Type': 'application/json' }, host: urlObj.hostname, port: urlObj.port, protocol: urlObj.protocol, method: method, path: urlObj.pathname } // proxy if (config.entity[baseEntity].proxy && config.entity[baseEntity].proxy.host) { const agent = new HttpsProxyAgent(config.entity[baseEntity].proxy.host) options.agent = agent // proxy if (config.entity[baseEntity].proxy.username && config.entity[baseEntity].proxy.password) { options.headers['Proxy-Authorization'] = 'Basic ' + Buffer.from(`${config.entity[baseEntity].proxy.username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.proxy.password`, configFile)}`).toString('base64') // using proxy with auth } } // merge any argument options - support basic auth using {auth: {username: "username", password: "password"} } if (opt) { const o = scimgateway.copyObj(opt) if (o.auth) { options.headers.Authorization = 'Basic ' + Buffer.from(`${o.auth.username}:${o.auth.password}`).toString('base64') delete o.auth } options = scimgateway.extendObj(options, o) } const cli = {} cli.options = options return cli // final client } const updateServiceClient = (baseEntity, obj) => { if (_serviceClient[baseEntity]) _serviceClient[baseEntity] = scimgateway.extendObj(_serviceClient[baseEntity], obj) // merge with argument options } // // doRequest - execute REST service // const doRequest = async (baseEntity, method, path, body, opt, retryCount) => { try { const cli = await getServiceClient(baseEntity, method, path, opt) const options = cli.options const result = await new Promise((resolve, reject) => { let dataString = '' if (body) { if (options.headers['Content-Type'].toLowerCase() === 'application/x-www-form-urlencoded') { if (typeof data === 'string') dataString = body else dataString = querystring.stringify(body) // JSON to query string syntax + URL encoded } else dataString = JSON.stringify(body) options.headers['Content-Length'] = Buffer.byteLength(dataString, 'utf8') } const reqType = (options.protocol.toLowerCase() === 'https:') ? https.request : http.request const req = reqType(options, (res) => { const { statusCode, statusMessage } = res // solving parallel problem (const + don't use res.statusCode) let responseString = '' res.setEncoding('utf-8') res.on('data', (chunk) => { responseString += chunk }) res.on('end', () => { const response = { statusCode: statusCode, statusMessage: statusMessage, body: null } try { if (responseString) response.body = JSON.parse(responseString) } catch (err) { response.body = responseString } if (statusCode < 200 || statusCode > 299) reject(new Error(JSON.stringify(response))) resolve(response) }) }) // req req.on('socket', (socket) => { socket.setTimeout(60000) // connect and wait timeout => socket hang up socket.on('timeout', function () { req.abort() }) }) req.on('error', (error) => { // also catching req.abort req.end() reject(error) }) if (dataString) req.write(dataString) req.end() }) // Promise scimgateway.logger.debug(`${pluginName}[${baseEntity}] doRequest ${method} ${options.protocol}//${options.host}${(options.port ? `:${options.port}` : '')}${path} Body = ${JSON.stringify(body)} Response = ${JSON.stringify(result)}`) return result } catch (err) { // includes failover/retry logic based on config baseUrls array scimgateway.logger.error(`${pluginName}[${baseEntity}] doRequest ${method} ${path} Body = ${JSON.stringify(body)} Error Response = ${err.message}`) if (!retryCount) retryCount = 0 let urlObj try { urlObj = new URL(path) } catch (err) {} if (!urlObj && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND')) { if (retryCount < config.entity[baseEntity].baseUrls.length) { retryCount++ updateServiceClient(baseEntity, { baseUrl: config.entity[baseEntity].baseUrls[retryCount - 1] }) scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${(config.entity[baseEntity].baseUrls.length > 1) ? 'failover ' : ''}retry[${retryCount}] using baseUrl = ${_serviceClient[baseEntity].baseUrl}`) const ret = await doRequest(baseEntity, method, path, body, opt, retryCount) // retry return ret // problem fixed } else { const newerr = new Error(err.message) newerr.message = newerr.message.replace('ECONNREFUSED', 'UnableConnectingService') // avoid returning ECONNREFUSED error newerr.message = newerr.message.replace('ENOTFOUND', 'UnableConnectingHost') // avoid returning ENOTFOUND error throw newerr } } else throw err // CA IM retries getUser failure once (retry 6 times on ECONNREFUSED) } } // doRequest // // Cleanup on exit // process.on('SIGTERM', () => { // kill }) process.on('SIGINT', () => { // Ctrl+C })