scimgateway 4.2.16 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,17 @@
1
1
  // =====================================================================================================================
2
- // File: plugin-azure-ad.js
2
+ // File: plugin-entra-id.js
3
3
  //
4
4
  // Author: Jarle Elshaug
5
5
  //
6
- // Purpose: Azure AD provisioning including licenses e.g. O365
6
+ // Purpose: Entra ID provisioning including licenses e.g. O365
7
7
  //
8
- // Prereq: Azure AD configuration:
8
+ // Prereq: Entra ID configuration:
9
9
  // Application key defined (clientsecret)
10
10
  // plugin-azure-ad.json configured with corresponding clientid and clientsecret
11
- // Application permission "Windows Azure Active Directory" - all "Application Permissions"
12
- // Application must be member of "User Account Administrator" (powershell import-Module MSOnline)
11
+ // Application permission: Directory.ReadWriteAll and Organization.ReadWrite.All
12
+ // Application must be member of "User Account Administrator" or "Global administrator"
13
13
  //
14
- // Notes: For CA Provisioning - Use ConnectorXpress, import metafile
14
+ // Notes: For Symantec/Broadcom/CA Provisioning - Use ConnectorXpress, import metafile
15
15
  // "node_modules\scimgateway\resources\Azure - ScimGateway.xml" for creating endpoint
16
16
  //
17
17
  // Using "Custom SCIM" attributes defined in scimgateway.endpointMap
@@ -129,7 +129,9 @@ for (const key in config.map.user) { // userAttributes = ['country', 'preferredL
129
129
  }
130
130
 
131
131
  for (const baseEntity in config.entity) { // ensure we have baseUrls in config and for this azure-ad plugin we overwrite any existing to hardcoded value ['https://graph.microsoft.com/beta']
132
- config.entity[baseEntity].baseUrls = ['https://graph.microsoft.com/beta'] // beta instead of 'v1.0' gives all user attributes when no $select
132
+ if (config.entity[baseEntity].oauth && config.entity[baseEntity].oauth.tenantIdGUID) {
133
+ config.entity[baseEntity].baseUrls = ['https://graph.microsoft.com/beta'] // beta instead of 'v1.0' gives all user attributes when no $select
134
+ }
133
135
  }
134
136
 
135
137
  const _serviceClient = {}
@@ -914,10 +916,104 @@ scimgateway.modifyServicePlan = async (baseEntity, id, ctx) => {
914
916
  throw new Error(`${action} error: ${action} is not supported`)
915
917
  }
916
918
 
919
+ // =================================================
920
+ // getUser
921
+ // addOn helper for plugin-azure-ad
922
+ // =================================================
923
+ const getUser = async (baseEntity, uid, attributes, ctx) => { // uid = id, userName (upn) or externalId (upn)
924
+ if (attributes.length < 1) {
925
+ attributes = userAttributes
926
+ }
927
+
928
+ const user = () => {
929
+ return new Promise((resolve, reject) => {
930
+ (async () => {
931
+ // const [attrs] = scimgateway.endpointMapper('outbound', attributes, config.map.user) // SCIM/CustomSCIM => endpoint attribute standard
932
+ const method = 'GET'
933
+ const path = `/users/${querystring.escape(uid)}?$expand=manager($select=userPrincipalName)` // beta returns all attributes or use: ?$select=${attrs.join()}
934
+ const body = null
935
+ try {
936
+ const response = await doRequest(baseEntity, method, path, body, ctx)
937
+ const userObj = response.body
938
+ if (!userObj) {
939
+ const err = new Error('Got empty response when retrieving data for ' + uid)
940
+ return reject(err)
941
+ }
942
+
943
+ let managerId
944
+ if (userObj.manager && userObj.manager.userPrincipalName) managerId = userObj.manager.userPrincipalName
945
+ delete userObj.manager
946
+ if (managerId) userObj.manager = managerId
947
+
948
+ resolve(userObj)
949
+ } catch (err) {
950
+ return reject(err)
951
+ }
952
+ })()
953
+ })
954
+ }
955
+
956
+ const license = () => {
957
+ return new Promise((resolve, reject) => {
958
+ (async () => {
959
+ if (!attributes.includes('servicePlan.value')) return resolve(null) // licenses not requested
960
+ const method = 'GET'
961
+ const path = `/users/${querystring.escape(uid)}/licenseDetails`
962
+ const body = null
963
+ const retObj = { servicePlan: [] }
964
+
965
+ try {
966
+ const response = await doRequest(baseEntity, method, path, body, ctx)
967
+ if (!response.body.value) {
968
+ const err = new Error('No content for license information ' + uid)
969
+ return reject(err)
970
+ } else {
971
+ if (response.body.value.length < 1) return resolve(null) // User with no licenses
972
+ for (let i = 0; i < response.body.value.length; i++) {
973
+ const skuPartNumber = response.body.value[i].skuPartNumber
974
+ for (let index = 0; index < response.body.value[i].servicePlans.length; index++) {
975
+ if (response.body.value[i].servicePlans[index].provisioningStatus === 'Success' ||
976
+ response.body.value[i].servicePlans[index].provisioningStatus === 'PendingInput') {
977
+ const servicePlan = { value: `${skuPartNumber}::${response.body.value[i].servicePlans[index].servicePlanName}` }
978
+ retObj.servicePlan.push(servicePlan)
979
+ }
980
+ }
981
+ }
982
+ }
983
+ resolve(retObj)
984
+ } catch (err) {
985
+ let statusCode
986
+ try { statusCode = JSON.parse(err.message).statusCode } catch (e) {}
987
+ if (statusCode === 404) return resolve(null) // user have no plans
988
+ return reject(err)
989
+ }
990
+ })()
991
+ })
992
+ }
993
+
994
+ // return Promise.all([user(), manager(), license()])
995
+ return Promise.all([user(), license()])
996
+ .then((results) => {
997
+ let retObj = {}
998
+ for (const i in results) { // merge async.parallell results to one
999
+ retObj = Object.assign(retObj, results[i])
1000
+ }
1001
+ return retObj
1002
+ })
1003
+ .catch((err) => {
1004
+ if (err.message.includes('empty response')) return null // no user found
1005
+ else throw (err)
1006
+ })
1007
+ }
1008
+
917
1009
  // =================================================
918
1010
  // helpers
919
1011
  // =================================================
920
1012
 
1013
+ //
1014
+ // start - REST endpoint template
1015
+ //
1016
+
921
1017
  const getClientIdentifier = (ctx) => {
922
1018
  if (!ctx?.request?.header?.authorization) return undefined
923
1019
  const [user, secret] = getCtxAuth(ctx)
@@ -936,6 +1032,85 @@ const getCtxAuth = (ctx) => {
936
1032
  else return [undefined, authToken] // bearer auth
937
1033
  }
938
1034
 
1035
+ //
1036
+ // getAccessToken - returns oauth accesstoken
1037
+ //
1038
+ const getAccessToken = async (baseEntity, ctx) => {
1039
+ await lock.acquire()
1040
+ const clientIdentifier = getClientIdentifier(ctx)
1041
+ const d = new Date() / 1000 // seconds (unix time)
1042
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken &&
1043
+ (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
1044
+ lock.release()
1045
+ return _serviceClient[baseEntity][clientIdentifier].accessToken
1046
+ }
1047
+
1048
+ const action = 'getAccessToken'
1049
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Retrieving accesstoken`)
1050
+
1051
+ const method = 'POST'
1052
+ const [, secret] = getCtxAuth(ctx) // if Auth PassTrough, secret from basic or bearer auth
1053
+ let tokenUrl
1054
+ let form
1055
+
1056
+ if (config.entity[baseEntity].oauth) {
1057
+ let resource
1058
+ try {
1059
+ const urlObj = new URL(config.entity[baseEntity].baseUrls[0])
1060
+ resource = urlObj.origin
1061
+ } catch (err) {
1062
+ resource = null
1063
+ }
1064
+ if (config.entity[baseEntity].oauth.tenantIdGUID) { // Azure
1065
+ tokenUrl = `https://login.microsoftonline.com/${config.entity[baseEntity].oauth.tenantIdGUID}/oauth2/token`
1066
+ } else {
1067
+ tokenUrl = `https://login.microsoftonline.com/${config.entity[baseEntity].oauth.tokenUrl}`
1068
+ }
1069
+ form = {
1070
+ grant_type: 'client_credentials',
1071
+ client_id: config.entity[baseEntity].oauth.clientId,
1072
+ client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.oauth.clientSecret`, configFile), // using config if no Auth PassThrough
1073
+ resource: resource // "https://graph.microsoft.com"
1074
+ }
1075
+ } else {
1076
+ const err = new Error(`[${action}] missing supported endpoint authentication configuration`)
1077
+ lock.release()
1078
+ throw (err)
1079
+ }
1080
+
1081
+ const options = {
1082
+ headers: {
1083
+ 'Content-Type': 'application/x-www-form-urlencoded' // body must be query string formatted (no JSON)
1084
+ }
1085
+ }
1086
+
1087
+ try {
1088
+ const response = await doRequest(baseEntity, method, tokenUrl, form, ctx, options)
1089
+ if (!response.body) {
1090
+ const err = new Error(`[${action}] No data retrieved from: ${method} ${tokenUrl}`)
1091
+ throw (err)
1092
+ }
1093
+ const jbody = response.body
1094
+ if (jbody.error) {
1095
+ const err = new Error(`[${action}] Error message: ${jbody.error_description}`)
1096
+ throw (err)
1097
+ } else if (!jbody.access_token || !jbody.expires_in) {
1098
+ const err = new Error(`[${action}] Error message: Retrieved invalid token response`)
1099
+ throw (err)
1100
+ }
1101
+
1102
+ const d = new Date() / 1000 // seconds (unix time)
1103
+ jbody.validTo = d + parseInt(jbody.expires_in) // instead of using expires_on (clock may not be in sync with NTP, AAD default expires_in = 3600 seconds)
1104
+ scimgateway.logger.silly(`${pluginName}[${baseEntity}] ${action}: AccessToken = ${jbody.access_token}`)
1105
+
1106
+ lock.release()
1107
+ return jbody
1108
+ } catch (err) {
1109
+ lock.release()
1110
+ throw (err)
1111
+ }
1112
+ }
1113
+
939
1114
  //
940
1115
  // getServiceClient - returns options needed for connection parameters
941
1116
  //
@@ -948,6 +1123,11 @@ const getCtxAuth = (ctx) => {
948
1123
  const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
949
1124
  const action = 'getServiceClient'
950
1125
 
1126
+ let authType
1127
+ if (config.entity[baseEntity].basicAuth) authType = 'basicAuth'
1128
+ else if (config.entity[baseEntity].oauth) authType = 'oauth'
1129
+ else if (config.entity[baseEntity].bearerAuth) authType = 'bearerAuth'
1130
+
951
1131
  let urlObj
952
1132
  if (!path) path = ''
953
1133
  try {
@@ -958,20 +1138,22 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
958
1138
  //
959
1139
 
960
1140
  const clientIdentifier = getClientIdentifier(ctx)
961
- if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken) { // serviceClient already exist - Azure plugin specific
1141
+ if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier]) { // serviceClient already exist - Azure plugin specific
962
1142
  scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Using existing client`)
963
- // check if token refresh is needed
964
- const d = new Date() / 1000 // seconds (unix time)
965
- if (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
966
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Accesstoken about to expire in ${_serviceClient[baseEntity][clientIdentifier].accessToken.validTo - d} seconds`)
967
- try {
968
- const accessToken = await getAccessToken(baseEntity, ctx)
969
- _serviceClient[baseEntity][clientIdentifier].accessToken = accessToken
970
- _serviceClient[baseEntity][clientIdentifier].options.headers.Authorization = ` Bearer ${accessToken.access_token}`
971
- } catch (err) {
972
- delete _serviceClient[baseEntity][clientIdentifier]
973
- const newErr = err
974
- throw newErr
1143
+ if (_serviceClient[baseEntity][clientIdentifier].accessToken) {
1144
+ // check if token refresh is needed when using oauth
1145
+ const d = new Date() / 1000 // seconds (unix time)
1146
+ if (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo < d + 30) { // less than 30 sec before token expiration
1147
+ scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Accesstoken about to expire in ${_serviceClient[baseEntity][clientIdentifier].accessToken.validTo - d} seconds`)
1148
+ try {
1149
+ const accessToken = await getAccessToken(baseEntity, ctx)
1150
+ _serviceClient[baseEntity][clientIdentifier].accessToken = accessToken
1151
+ _serviceClient[baseEntity][clientIdentifier].options.headers.Authorization = ` Bearer ${accessToken.access_token}`
1152
+ } catch (err) {
1153
+ delete _serviceClient[baseEntity][clientIdentifier]
1154
+ const newErr = err
1155
+ throw newErr
1156
+ }
975
1157
  }
976
1158
  }
977
1159
  } else {
@@ -982,19 +1164,18 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
982
1164
  const err = new Error(`Base URL have baseEntity=${baseEntity}, and configuration file ${pluginName}.json is missing required baseEntity configuration for ${baseEntity}`)
983
1165
  throw err
984
1166
  }
985
-
986
- // Azure plugin specific
987
- const accessToken = await getAccessToken(baseEntity, ctx)
988
-
1167
+ if (!config.entity[baseEntity].baseUrls || !Array.isArray(config.entity[baseEntity].baseUrls) || config.entity[baseEntity].baseUrls.length < 1) {
1168
+ const err = new Error(`missing configuration entity.${baseEntity}.baseUrls`)
1169
+ throw err
1170
+ }
989
1171
  urlObj = new URL(config.entity[baseEntity].baseUrls[0])
990
1172
  const param = {
991
1173
  baseUrl: config.entity[baseEntity].baseUrls[0],
992
- accessToken: accessToken, // Azure plugin specific
993
1174
  options: {
994
1175
  json: true, // json-object response instead of string
995
1176
  headers: {
996
1177
  'Content-Type': 'application/json',
997
- Authorization: ` Bearer ${accessToken.access_token}`
1178
+ Accept: 'application/json'
998
1179
  },
999
1180
  host: urlObj.hostname,
1000
1181
  port: urlObj.port, // null if https and 443 defined in url
@@ -1003,6 +1184,37 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
1003
1184
  }
1004
1185
  }
1005
1186
 
1187
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough using ctx header
1188
+ param.options.headers.Authorization = ctx.request.header.authorization
1189
+ } else {
1190
+ switch (authType) {
1191
+ case 'basicAuth':
1192
+ if (!config.entity[baseEntity].basicAuth.username || !config.entity[baseEntity].basicAuth.password) {
1193
+ const err = new Error(`missing configuration entity.${baseEntity}.basicAuth.username/password`)
1194
+ throw err
1195
+ }
1196
+ param.options.headers.Authorization = 'Basic ' + Buffer.from(`${config.entity[baseEntity].basicAuth.username}:${scimgateway.getPassword(`endpoint.entity.${baseEntity}.basicAuth.password`, configFile)}`).toString('base64')
1197
+ break
1198
+ case 'oauth':
1199
+ if (!config.entity[baseEntity].oauth.clientId || !config.entity[baseEntity].oauth.clientSecret) {
1200
+ const err = new Error(`missing configuration entity.${baseEntity}.oauth.clientId/clientSecret`)
1201
+ throw err
1202
+ }
1203
+ param.accessToken = await getAccessToken(baseEntity, ctx)
1204
+ param.options.headers.Authorization = `Bearer ${param.accessToken.access_token}`
1205
+ break
1206
+ case 'bearerAuth':
1207
+ if (!config.entity[baseEntity].bearerAuth.token) {
1208
+ const err = new Error(`missing configuration entity.${baseEntity}.bearerAuth.token`)
1209
+ throw err
1210
+ }
1211
+ param.options.headers.Authorization = 'Bearer ' + Buffer.from(`${scimgateway.getPassword(`endpoint.entity.${baseEntity}.bearerAuth.token`, configFile)}`).toString('base64')
1212
+ break
1213
+ default:
1214
+ // no auth
1215
+ }
1216
+ }
1217
+
1006
1218
  // proxy
1007
1219
  if (config.entity[baseEntity].proxy && config.entity[baseEntity].proxy.host) {
1008
1220
  const agent = new HttpsProxyAgent(config.entity[baseEntity].proxy.host)
@@ -1012,12 +1224,14 @@ const getServiceClient = async (baseEntity, method, path, opt, ctx) => {
1012
1224
  }
1013
1225
  }
1014
1226
 
1227
+ // config options
1228
+ if (config.entity[baseEntity].options) param.options = scimgateway.extendObj(param.options, config.entity[baseEntity].options)
1229
+
1015
1230
  if (!_serviceClient[baseEntity]) _serviceClient[baseEntity] = {}
1016
1231
  if (!_serviceClient[baseEntity][clientIdentifier]) _serviceClient[baseEntity][clientIdentifier] = {}
1017
-
1018
1232
  _serviceClient[baseEntity][clientIdentifier] = param // serviceClient created
1019
1233
 
1020
- // Azure plugin specific (note, not using [clientIdentifier])
1234
+ // OData support - note, not using [clientIdentifier]
1021
1235
  _serviceClient[baseEntity].nextLink = {}
1022
1236
  _serviceClient[baseEntity].nextLink.users = null // Azure users pagination
1023
1237
  _serviceClient[baseEntity].nextLink.groups = null // Azure groups pagination
@@ -1173,150 +1387,8 @@ const doRequest = async (baseEntity, method, path, body, ctx, opt, retryCount) =
1173
1387
  } // doRequest
1174
1388
 
1175
1389
  //
1176
- // getAccessToken - returns oauth jwt accesstoken
1390
+ // end - REST endpoint template
1177
1391
  //
1178
- const getAccessToken = async (baseEntity, ctx) => {
1179
- await lock.acquire()
1180
- const clientIdentifier = getClientIdentifier(ctx)
1181
- const d = new Date() / 1000 // seconds (unix time)
1182
- if (_serviceClient[baseEntity] && _serviceClient[baseEntity][clientIdentifier] && _serviceClient[baseEntity][clientIdentifier].accessToken &&
1183
- (_serviceClient[baseEntity][clientIdentifier].accessToken.validTo >= d + 30)) { // avoid simultaneously token requests
1184
- lock.release()
1185
- return _serviceClient[baseEntity][clientIdentifier].accessToken
1186
- }
1187
-
1188
- const action = 'getAccessToken'
1189
- scimgateway.logger.debug(`${pluginName}[${baseEntity}] ${action}: Retrieving accesstoken`)
1190
-
1191
- const req = `https://login.microsoftonline.com/${config.entity[baseEntity].tenantIdGUID}/oauth2/token`
1192
- const method = 'POST'
1193
-
1194
- const [, secret] = getCtxAuth(ctx) // if Auth PassTrough, secret from basic or bearer auth
1195
- const form = { // query string formatted
1196
- grant_type: 'client_credentials',
1197
- client_id: config.entity[baseEntity].clientId,
1198
- client_secret: secret || scimgateway.getPassword(`endpoint.entity.${baseEntity}.clientSecret`, configFile), // using config if no Auth PassThrough
1199
- resource: 'https://graph.microsoft.com'
1200
- }
1201
-
1202
- const options = {
1203
- headers: {
1204
- 'Content-Type': 'application/x-www-form-urlencoded' // body must be query string formatted (no JSON)
1205
- }
1206
- }
1207
-
1208
- try {
1209
- const response = await doRequest(baseEntity, method, req, form, ctx, options)
1210
- if (!response.body) {
1211
- const err = new Error(`[${action}] No data retrieved from: ${method} ${req}`)
1212
- throw (err)
1213
- }
1214
- const jbody = response.body
1215
- if (jbody.error) {
1216
- const err = new Error(`[${action}] Error message: ${jbody.error_description}`)
1217
- throw (err)
1218
- } else if (!jbody.access_token || !jbody.expires_in) {
1219
- const err = new Error(`[${action}] Error message: Retrieved invalid token response`)
1220
- throw (err)
1221
- }
1222
-
1223
- const d = new Date() / 1000 // seconds (unix time)
1224
- jbody.validTo = d + parseInt(jbody.expires_in) // instead of using expires_on (clock may not be in sync with NTP, AAD default expires_in = 3600 seconds)
1225
- scimgateway.logger.silly(`${pluginName}[${baseEntity}] ${action}: AccessToken = ${jbody.access_token}`)
1226
-
1227
- lock.release()
1228
- return jbody
1229
- } catch (err) {
1230
- lock.release()
1231
- throw (err)
1232
- }
1233
- }
1234
-
1235
- const getUser = async (baseEntity, uid, attributes, ctx) => { // uid = id, userName (upn) or externalId (upn)
1236
- if (attributes.length < 1) {
1237
- attributes = userAttributes
1238
- }
1239
-
1240
- const user = () => {
1241
- return new Promise((resolve, reject) => {
1242
- (async () => {
1243
- // const [attrs] = scimgateway.endpointMapper('outbound', attributes, config.map.user) // SCIM/CustomSCIM => endpoint attribute standard
1244
- const method = 'GET'
1245
- const path = `/users/${querystring.escape(uid)}?$expand=manager($select=userPrincipalName)` // beta returns all attributes or use: ?$select=${attrs.join()}
1246
- const body = null
1247
- try {
1248
- const response = await doRequest(baseEntity, method, path, body, ctx)
1249
- const userObj = response.body
1250
- if (!userObj) {
1251
- const err = new Error('Got empty response when retrieving data for ' + uid)
1252
- return reject(err)
1253
- }
1254
-
1255
- let managerId
1256
- if (userObj.manager && userObj.manager.userPrincipalName) managerId = userObj.manager.userPrincipalName
1257
- delete userObj.manager
1258
- if (managerId) userObj.manager = managerId
1259
-
1260
- resolve(userObj)
1261
- } catch (err) {
1262
- return reject(err)
1263
- }
1264
- })()
1265
- })
1266
- }
1267
-
1268
- const license = () => {
1269
- return new Promise((resolve, reject) => {
1270
- (async () => {
1271
- if (!attributes.includes('servicePlan.value')) return resolve(null) // licenses not requested
1272
- const method = 'GET'
1273
- const path = `/users/${querystring.escape(uid)}/licenseDetails`
1274
- const body = null
1275
- const retObj = { servicePlan: [] }
1276
-
1277
- try {
1278
- const response = await doRequest(baseEntity, method, path, body, ctx)
1279
- if (!response.body.value) {
1280
- const err = new Error('No content for license information ' + uid)
1281
- return reject(err)
1282
- } else {
1283
- if (response.body.value.length < 1) return resolve(null) // User with no licenses
1284
- for (let i = 0; i < response.body.value.length; i++) {
1285
- const skuPartNumber = response.body.value[i].skuPartNumber
1286
- for (let index = 0; index < response.body.value[i].servicePlans.length; index++) {
1287
- if (response.body.value[i].servicePlans[index].provisioningStatus === 'Success' ||
1288
- response.body.value[i].servicePlans[index].provisioningStatus === 'PendingInput') {
1289
- const servicePlan = { value: `${skuPartNumber}::${response.body.value[i].servicePlans[index].servicePlanName}` }
1290
- retObj.servicePlan.push(servicePlan)
1291
- }
1292
- }
1293
- }
1294
- }
1295
- resolve(retObj)
1296
- } catch (err) {
1297
- let statusCode
1298
- try { statusCode = JSON.parse(err.message).statusCode } catch (e) {}
1299
- if (statusCode === 404) return resolve(null) // user have no plans
1300
- return reject(err)
1301
- }
1302
- })()
1303
- })
1304
- }
1305
-
1306
- // return Promise.all([user(), manager(), license()])
1307
- return Promise.all([user(), license()])
1308
- .then((results) => {
1309
- let retObj = {}
1310
- for (const i in results) { // merge async.parallell results to one
1311
- retObj = Object.assign(retObj, results[i])
1312
- }
1313
- return retObj
1314
- })
1315
- .catch((err) => {
1316
- if (err.message.includes('empty response')) return null // no user found
1317
- else throw (err)
1318
- })
1319
- }
1320
1392
 
1321
1393
  //
1322
1394
  // Cleanup on exit
@@ -202,7 +202,16 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
202
202
  throw new Error(`${action} error: not supporting groups member of user filtering: ${getObj.rawFilter}`)
203
203
  } else {
204
204
  // optional - simpel filtering
205
- throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
205
+ if (getObj.operator === 'eq') {
206
+ ldapOptions = {
207
+ filter: `&${getObjClassFilter(baseEntity, 'user')}(${getObj.attribute}=${getObj.value})`, // &(objectClass=user)(objectClass=person)(objectClass=organizationalPerson)(objectClass=top)(employeeNumber=123)
208
+ scope: scope,
209
+ attributes: attrs
210
+ }
211
+ if (config.entity[baseEntity].ldap.userFilter) ldapOptions.filter += config.entity[baseEntity].ldap.userFilter
212
+ } else {
213
+ throw new Error(`${action} error: not supporting simpel filtering: ${getObj.rawFilter}`)
214
+ }
206
215
  }
207
216
  } else if (getObj.rawFilter) {
208
217
  // optional - advanced filtering having and/or/not - use getObj.rawFilter
@@ -284,8 +293,8 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
284
293
  if (!userBase) userBase = config.entity[baseEntity].ldap.userBase
285
294
 
286
295
  // convert SCIM attributes to endpoint attributes according to config.map
287
- const [endpointObj, err] = scimgateway.endpointMapper('outbound', userObj, config.map.user)
288
- if (err) throw new Error(`${action} error: ${err.message}`)
296
+ const [endpointObj] = scimgateway.endpointMapper('outbound', userObj, config.map.user)
297
+ // if (err) throw new Error(`${action} error: ${err.message}`) // use above [endpointObj, err] to catch non supported attributes
289
298
 
290
299
  // endpoint spesific attribute handling
291
300
  if (endpointObj.sAMAccountName !== undefined) { // Active Directory
@@ -366,8 +375,8 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
366
375
  const groups = attrObj.groups
367
376
  delete attrObj.groups // make sure to be removed from attrObj
368
377
 
369
- const [groupsAttr, err] = scimgateway.endpointMapper('outbound', 'groups.value', config.map.user)
370
- if (err) throw new Error(`${action} error: ${err.message}`)
378
+ const [groupsAttr] = scimgateway.endpointMapper('outbound', 'groups.value', config.map.user)
379
+ // if (err) throw new Error(`${action} error: ${err.message}`) // use above [endpointObj, err] to catch non supported attributes
371
380
 
372
381
  const body = { add: { }, remove: { } }
373
382
  body.add[groupsAttr] = []
@@ -416,8 +425,8 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
416
425
  if (JSON.stringify(attrObj) === '{}') return null // only groups included
417
426
 
418
427
  // convert SCIM attributes to endpoint attributes according to config.map
419
- const [endpointObj, err] = scimgateway.endpointMapper('outbound', attrObj, config.map.user)
420
- if (err) throw new Error(`${action} error: ${err.message}`)
428
+ const [endpointObj] = scimgateway.endpointMapper('outbound', attrObj, config.map.user)
429
+ // if (err) throw new Error(`${action} error: ${err.message}`) // use above [endpointObj, err] to catch non supported attributes
421
430
 
422
431
  // endpoint spesific attribute handling
423
432
  if (endpointObj.userAccountControl !== undefined) { // SCIM "active" - Active Directory