@keepsolutions/scimgateway
Version:
Using SCIM protocol as a gateway for user provisioning to other endpoints
497 lines (455 loc) • 21.1 kB
JavaScript
// =================================================================================
// File: plugin-mssql.js
//
// Author: Jarle Elshaug
//
// Purpose: SQL user-provisioning
//
// Prereq:
// TABLE [dbo].[User](
// [UserID] [varchar](50) NOT NULL,
// [Enabled] [varchar](50) NULL,
// [Password] [varchar](50) NULL,
// [FirstName] [varchar](50) NULL,
// [MiddleName] [varchar](50) NULL,
// [LastName] [varchar](50) NULL,
// [Email] [varchar](50) NULL,
// [MobilePhone] [varchar](50) NULL
// )
//
// Supported attributes:
//
// GlobalUser Template Scim Endpoint
// --------------------------------------------------------------------------------------------
// User name %AC% userName UserID
// Suspended (auto included) active Enabled
// Password %P% password Password
// First Name %UF% name.givenName FirstName
// Middle Name %UMN% name.middleName MiddleName
// Last Name %UL% name.familyName LastName
// Email %UE% (Emails, type=Work) emails.work emailAddress
// Phone %UP% (Phone Numbers, type=Work) phoneNumbers.work phoneNumber
//
// =================================================================================
const Connection = require('tedious').Connection
const Request = require('tedious').Request
// 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 is mandatory
'active', // active is mandatory
'password',
'name.givenName',
'name.middleName',
'name.familyName',
// "emails", // accepts all multivalues for this key
'emails.work', // accepts multivalues if type value equal work (lowercase)
// "phoneNumbers",
'phoneNumbers.work'
]
let config = require(configFile).endpoint
config = scimgateway.processExtConfig(pluginName, config) // add any external config process.env and process.file
// mandatory plugin initialization - end
const sqlPassword = scimgateway.getPassword('endpoint.connection.authentication.options.password', configFile)
config.connection.authentication.options.password = sqlPassword // Connection using config.connection
// =================================================
// exploreUsers
// =================================================
scimgateway.exploreUsers = async (baseEntity, attributes, startIndex, count) => {
try {
const action = 'exploreUsers'
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" attributes=${attributes} startIndex=${startIndex} count=${count}`)
return await new Promise((resolve, reject) => {
const ret = { // itemsPerPage will be set by scimgateway
Resources: [],
totalResults: null
}
const connection = new Connection(config.connection)
connection.on('connect', function (err) {
if (err) {
const e = new Error(`exploreUsers MSSQL client connect error: ${err.message}`)
return reject(e)
}
const sqlQuery = 'select UserID from [User]'
const request = new Request(sqlQuery, function (err, rowCount, rows) {
if (err) {
connection.close()
const e = new Error(`exploreUsers MSSQL client request: ${sqlQuery} Error: ${err.message}`)
return reject(e)
}
for (const row in rows) {
const id = rows[row].UserID.value
const userName = rows[row].UserID.value
const scimUser = { // returning userName and id
userName: userName,
id: id
}
ret.Resources.push(scimUser)
}
connection.close()
resolve(ret) // all explored users
}) // request
connection.execSql(request)
}) // connection
connection.connect() // initialize the connection
}) // Promise
} 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 ret = {
Resources: [],
totalResults: null
}
return ret
}
// =================================================
// 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}`)
if (getObj.filter !== 'userName' && getObj.filter !== 'externalId' && getObj.filter !== 'id') {
throw new Error(`plugin do not support handling "${action}" ${getObj.filter}`)
}
try {
return await new Promise((resolve, reject) => {
const connection = new Connection(config.connection)
connection.on('connect', function (err) {
if (err) {
const e = new Error(`getUser MSSQL client connect error: ${err.message}`)
reject(e)
}
// all endpoint supported attributes
const sqlQuery = `select UserID, Enabled, FirstName, MiddleName, LastName, Email, MobilePhone from [User] where UserID = '${getObj.identifier}'`
const request = new Request(sqlQuery, function (err, rowCount, rows) {
if (err) {
connection.close()
const e = new Error(`Explore-Users connect: MSSQL client request: ${sqlQuery} Error: ${err.message}`)
return reject(e)
}
if (rowCount !== 1) {
connection.close()
resolve(null) // user not found
}
const userObj = {
userName: rows[0].UserID.value,
id: rows[0].UserID.value,
active: rows[0].Enabled.value,
name: {
givenName: rows[0].FirstName.value || '',
middleName: rows[0].MiddleName.value || '',
familyName: rows[0].LastName.value || ''
},
emails: (rows[0].Email.value) ? [{
value: rows[0].Email.value,
type: 'work'
}] : null,
phoneNumbers: (rows[0].MobilePhone.value) ? [{
value: rows[0].MobilePhone.value,
type: 'work'
}] : null
}
connection.close()
if (!attributes) resolve(userObj) // return user having all attributtes
// return according to attributes (can be skipped)
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 (userObj[attr[0]]) {
if (attr.length === 1) ret[attr[0]] = userObj[attr[0]]
else if (userObj[attr[0]][attr[1]]) { // name.familyName
if (!ret[attr[0]]) ret[attr[0]] = {}
ret[attr[0]][attr[1]] = userObj[attr[0]][attr[1]]
} else if (Array.isArray(userObj[attr[0]])) { // emails.value / phoneNumbers.type
if (!ret[attr[0]]) ret[attr[0]] = []
const arr = userObj[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
}
}
}
}
}
resolve(ret)
})
connection.execSql(request)
}) // connect
connection.connect() // initialize the connection
}) // Promise
} catch (err) {
const newErr = err
throw newErr
}
}
// =================================================
// createUser
// =================================================
scimgateway.createUser = async (baseEntity, userObj) => {
try {
const action = 'createUser'
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" userObj=${JSON.stringify(userObj)}`)
return await new Promise((resolve, reject) => {
const notValid = scimgateway.notValidAttributes(userObj, validScimAttr)
if (notValid) {
const err = new Error(`unsupported scim attributes: ${notValid} ` +
`(supporting only these attributes: ${validScimAttr.toString()})`
)
return reject(err)
}
if (!userObj.name) userObj.name = {}
if (!userObj.emails) userObj.emails = { work: {} }
if (!userObj.phoneNumbers) userObj.phoneNumbers = { work: {} }
const insert = {
UserID: `'${userObj.userName}'`,
Enabled: (userObj.active) ? `'${userObj.active}'` : '\'false\'',
Password: (userObj.password) ? `'${userObj.password}'` : null,
FirstName: (userObj.name.givenName) ? `'${userObj.name.givenName}'` : null,
MiddleName: (userObj.name.middleName) ? `'${userObj.name.middleName}'` : null,
LastName: (userObj.name.familyName) ? `'${userObj.name.familyName}'` : null,
MobilePhone: (userObj.phoneNumbers.work.value) ? `'${userObj.phoneNumbers.work.value}'` : null,
Email: (userObj.emails.work.value) ? `'${userObj.emails.work.value}'` : null
}
const connection = new Connection(config.connection)
connection.on('connect', function (err) {
if (err) {
const e = new Error(`createUser MSSQL client connect error: ${err.message}`)
return reject(e)
}
const sqlQuery = `insert into [User] (UserID, Enabled, Password, FirstName, MiddleName, LastName, Email, MobilePhone)
values (${insert.UserID}, ${insert.Enabled}, ${insert.Password}, ${insert.FirstName}, ${insert.MiddleName}, ${insert.LastName}, ${insert.Email}, ${insert.MobilePhone})`
const request = new Request(sqlQuery, function (err, rowCount, rows) {
if (err) {
connection.close()
const e = new Error(`createUser MSSQL client request: ${sqlQuery} error: ${err.message}`)
return reject(e)
}
connection.close()
resolve(null)
}) // request
connection.execSql(request)
}) // connection
connection.connect() // initialize the connection
}) // Promise
} catch (err) {
const newErr = err
throw newErr
}
}
// =================================================
// deleteUser
// =================================================
scimgateway.deleteUser = async (baseEntity, id) => {
try {
const action = 'deleteUser'
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
return await new Promise((resolve, reject) => {
const connection = new Connection(config.connection)
connection.on('connect', function (err) {
if (err) {
const e = new Error(`deleteUser MSSQL client connect error: ${err.message}`)
return reject(e)
}
const sqlQuery = `delete from [User] where UserID = '${id}'`
const request = new Request(sqlQuery, function (err, rowCount, rows) {
if (err) {
connection.close()
const e = new Error(`deleteUser MSSQL client request: ${sqlQuery} error: ${err.message}`)
return reject(e)
}
connection.close()
resolve(null)
}) // request
connection.execSql(request)
}) // connection
connection.connect() // initialize the connection
}) // Promise
} catch (err) {
const newErr = err
throw newErr
}
}
// =================================================
// modifyUser
// =================================================
scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
try {
const action = 'modifyUser'
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
return await new Promise((resolve, reject) => {
const notValid = scimgateway.notValidAttributes(attrObj, validScimAttr)
if (notValid) {
const err = new Error(`unsupported scim attributes: ${notValid} ` +
`(supporting only these attributes: ${validScimAttr.toString()})`
)
return reject(err)
}
if (!attrObj.name) attrObj.name = {}
if (!attrObj.emails) attrObj.emails = { work: {} }
if (!attrObj.phoneNumbers) attrObj.phoneNumbers = { work: {} }
let sql = ''
if (attrObj.active !== undefined) sql += `Enabled='${attrObj.active}',`
if (attrObj.password !== undefined) {
if (attrObj.password === '') sql += 'Password=null,'
else sql += `Password='${attrObj.password}',`
}
if (attrObj.name.givenName !== undefined) {
if (attrObj.name.givenName === '') sql += 'FirstName=null,'
else sql += `FirstName='${attrObj.name.givenName}',`
}
if (attrObj.name.middleName !== undefined) {
if (attrObj.name.middleName === '') sql += 'MiddleName=null,'
else sql += `MiddleName='${attrObj.name.middleName}',`
}
if (attrObj.name.familyName !== undefined) {
if (attrObj.name.familyName === '') sql += 'LastName=null,'
else sql += `LastName='${attrObj.name.familyName}',`
}
if (attrObj.phoneNumbers.work.value !== undefined) {
if (attrObj.phoneNumbers.work.value === '') sql += 'MobilePhone=null,'
else sql += `MobilePhone='${attrObj.phoneNumbers.work.value}',`
}
if (attrObj.emails.work.value !== undefined) {
if (attrObj.emails.work.value === '') sql += 'Email=null,'
else sql += `Email='${attrObj.emails.work.value}',`
}
sql = sql.substr(0, sql.length - 1) // remove trailing ","
const connection = new Connection(config.connection)
connection.on('connect', function (err) {
if (err) {
const e = new Error(`modifyUser MSSQL client connect error: ${err.message}`)
return reject(e)
}
const sqlQuery = `update [User] set ${sql} where UserID like '${id}'`
const request = new Request(sqlQuery, function (err, rowCount, rows) {
if (err) {
connection.close()
const e = new Error(`modifyUser MSSQL client request: ${sqlQuery} error: ${err.message}`)
return reject(e)
}
connection.close()
resolve(null)
}) // request
connection.execSql(request)
}) // connection
connection.connect() // initialize the connection
}) // Promise
} 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}`)
return null // groups not implemented
}
// =================================================
// 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 = []
return arrRet // groups not implemented
}
// =================================================
// 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)}`)
// groupObj.displayName contains the group to be created
// if supporting create group we need some endpoint logic here
const err = new Error(`Create group is not supported by ${pluginName}`)
throw err
}
// =================================================
// deleteGroup
// =================================================
scimgateway.deleteGroup = async (baseEntity, id) => {
const action = 'deleteGroup'
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id}`)
// if supporting delete group we need some endpoint logic here
const err = new Error(`Delete group is not supported by ${pluginName}`)
throw err
}
// =================================================
// modifyGroup
// =================================================
scimgateway.modifyGroup = async (baseEntity, id, attrObj) => {
const action = 'modifyGroup'
scimgateway.logger.debug(`${pluginName}[${baseEntity}] handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
return null
}
// =================================================
// helpers
// =================================================
//
// Cleanup on exit
//
process.on('SIGTERM', () => { // kill
})
process.on('SIGINT', () => { // Ctrl+C
})