scimgateway 4.1.11 → 4.1.13

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.
package/README.md CHANGED
@@ -17,12 +17,12 @@ Validated through IdP's:
17
17
  Latest news:
18
18
 
19
19
  - **BREAKING**: [SCIM Stream](https://elshaug.xyz/docs/scim-stream) is the modern way of user provisioning letting clients subscribe to messages instead of traditional IGA top-down provisioning. SCIM Stream includes **SCIM Stream Gateway**, the next generation SCIM Gateway that supports message subscription and automated provisioning
20
- - Supporting OAuth Client Credentials authentication
20
+ - Supports OAuth Client Credentials authentication
21
21
  - Major version v4.0.0. getUsers() and getGroups() replacing some deprecated methods. No limitations on filtering/sorting. Admin user access can be linked to specific baseEntities. New MongoDB plugin
22
22
  - ipAllowList for restricting access to allowlisted IP addresses or subnets e.g. Azure AD IP-range
23
23
  - General LDAP plugin configured for Active Directory
24
24
  - [PlugSSO](https://elshaug.xyz/docs/plugsso) using SCIM Gateway
25
- - Authentication configuration allowing more than one admin user including option for readOnly
25
+ - Each authentication configuration allowing more than one admin user including option for readOnly
26
26
  - Codebase moved from callback of h... to the the promise(d) land of async/await
27
27
  - Supports configuration by environments and external files
28
28
  - Health monitoring through "/ping" URL, and option for error notifications by email
@@ -49,7 +49,7 @@ SCIM Gateway becomes a standalone SCIM endpoint
49
49
  Demonstrates user provisioning towards document-oriented database
50
50
  Using [LokiJS](https://github.com/techfort/LokiJS) for a fast, in-memory document-oriented database (much like MongoDB/PouchDB)
51
51
  Default gives two predefined test users loaded using in-memory only (no persistence)
52
- Setting `{"persistence": true}` gives persistence file store (no test users)
52
+ Configuration `{"persistence": true}` gives persistence file store (no test users)
53
53
  Example of a fully functional SCIM Gateway plugin
54
54
 
55
55
  * **MongoDB** (NoSQL Document-Oriented Database)
@@ -379,11 +379,11 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
379
379
 
380
380
  Note, we should normally use certificate (https) for communicating with SCIM Gateway unless we install ScimGatway locally on the manager (e.g. on the CA Connector Server). When installed on the manager, we could use `http://localhost:port` or `http://127.0.0.1:port` which will not be passed down to the data link layer for transmission. We could then also set {"localhostonly": true}
381
381
 
382
- - **ipAllowList** - Array of one or more IPv4/IPv6 subnets (CIDR) allowed for incoming traffic. E.g. using Azure AD as IdP, we would like to restrict access to IP addresses used by Azure AD. Azure IP-range can be downloaded from: [https://azureipranges.azurewebsites.net](https://azureipranges.azurewebsites.net), enter **AzureActiveDirectory** in the search list and select JSON download. Copy the "addressPrefixes" array content and paste into ipAllowList array. CIDR single IP-host syntax is a.b.c.d/32. Note, front-end HTTP proxy or a load balancer must include **X-Forwarded-For** header. Configuration example:
382
+ - **ipAllowList** - Array of one or more IPv4/IPv6 subnets (CIDR) allowed for incoming traffic. E.g. using Azure AD as IdP, we would like to restrict access to IP addresses used by Azure AD. Azure IP-range can be downloaded from: [https://azureipranges.azurewebsites.net](https://azureipranges.azurewebsites.net), enter **AzureActiveDirectory** in the search list and select JSON download. Copy the "addressPrefixes" array content and paste into ipAllowList array. CIDR single IP-host syntax is a.b.c.d/32. Note, front-end HTTP proxy or a load balancer must include client IP-address in the **X-Forwarded-For** header. Configuration example:
383
383
 
384
384
  "ipAllowList": [
385
- "13.66.60.119/32",
386
- "13.66.143.220/30",
385
+ "13.64.151.161/32",
386
+ "13.66.141.64/27",
387
387
  ...
388
388
  "2603:1056:2000::/48",
389
389
  "2603:1057:2::/48"
@@ -1146,6 +1146,18 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1146
1146
 
1147
1147
  ## Change log
1148
1148
 
1149
+ ### v4.1.13
1150
+
1151
+ [Fixed]
1152
+
1153
+ - Do not create logs directory or log-file when configuration `log.loglevel.file` not defined or set to `"off"`. This fix will allow SCIM Gateway to run on systems having read-only disk like Google Cloud App Engine Standard
1154
+
1155
+ ### v4.1.12
1156
+
1157
+ [Added]
1158
+
1159
+ - Dependencies bump
1160
+
1149
1161
  ### v4.1.11
1150
1162
 
1151
1163
  [Fixed]
@@ -1187,13 +1199,16 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1187
1199
  - Symantec/Broadcom/CA ConnectorXpress configuration file `config\resources\Azure - ScimGateway.xml` for defining the Azure endpoint, have been updated with some new attributes according to plugin-azure-ad.json attribute mappings
1188
1200
 
1189
1201
  ### v4.1.6
1190
- [Added]
1202
+
1203
+ [Added]
1204
+
1191
1205
  - Dependencies bump
1192
1206
 
1193
1207
  ### v4.1.5
1208
+
1194
1209
  [Added]
1195
1210
 
1196
- Announcing some SCIM Gateway related news:
1211
+ SCIM Gateway related news:
1197
1212
 
1198
1213
  - [SCIM Stream](https://elshaug.xyz/docs/scim-stream) is the modern way of user provisioning letting clients subscribe to messages instead of traditional IGA top-down provisioning. SCIM Stream includes **SCIM Stream Gateway**, the next generation SCIM Gateway that supports message subscription and automated provisioning
1199
1214
 
package/lib/logger.js CHANGED
@@ -44,12 +44,12 @@ const Log = function (config, logfile, _this) { // { loglevel: { file: "debug",
44
44
 
45
45
  const maskSecret = winston.format((info, opts) => {
46
46
  // mask json secrets
47
- var rePattern = new RegExp(reJson, 'i')
47
+ let rePattern = new RegExp(reJson, 'i')
48
48
  let msg = info.message
49
49
  let endPos = msg.length - 1
50
50
  let found = false
51
51
  do {
52
- var arrMatches = msg.substring(0, endPos).match(rePattern)
52
+ const arrMatches = msg.substring(0, endPos).match(rePattern)
53
53
  if (Array.isArray(arrMatches) && arrMatches.length === 3) {
54
54
  arrMatches[2] = arrMatches[2].replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // escaping special regexp characters
55
55
  msg = msg.replace(new RegExp(arrMatches[2], 'g'), '********')
@@ -62,7 +62,7 @@ const Log = function (config, logfile, _this) { // { loglevel: { file: "debug",
62
62
  rePattern = new RegExp(reXml, 'i')
63
63
  endPos = msg.length - 1
64
64
  do {
65
- arrMatches = msg.substring(0, endPos).match(rePattern)
65
+ const arrMatches = msg.substring(0, endPos).match(rePattern)
66
66
  if (Array.isArray(arrMatches) && arrMatches.length === 3) {
67
67
  arrMatches[2] = arrMatches[2].replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
68
68
  msg = msg.replace(new RegExp('>' + arrMatches[2] + '<', 'g'), '>********<')
@@ -98,38 +98,48 @@ const Log = function (config, logfile, _this) { // { loglevel: { file: "debug",
98
98
  )
99
99
  }
100
100
 
101
- Log.prototype.logger = winston.createLogger({
102
- format: winston.format.combine(
103
- maskSecret()
104
- ),
105
- transports: [
106
- new winston.transports.File({
107
- level: (config.loglevel.file && arrValidLevel.includes(config.loglevel.file.toLowerCase())) ? config.loglevel.file : 'debug',
108
- filename: logfile,
109
- handleExceptions: true,
110
- format: fileFormat,
111
- maxsize: 1024 * 1024 * 20, // 20 MB
112
- maxFiles: 5
113
- }),
114
- new winston.transports.Console({ // note, console logging is synchronous e.g. node.js halts when console window is scrolled
115
- level: (config.loglevel.console && arrValidLevel.includes(config.loglevel.console.toLowerCase())) ? config.loglevel.console : 'debug',
116
- handleExceptions: true,
117
- stderrLevels: ['error'],
118
- format: (config.colorize) ? consoleFormat : Log.prototype.unColorize()
101
+ Log.prototype.logger = () => {
102
+ if (config.loglevel.file) {
103
+ return winston.createLogger({
104
+ format: winston.format.combine(
105
+ maskSecret()
106
+ ),
107
+ transports: [
108
+ new winston.transports.Console({ // note, console logging is synchronous e.g. node.js halts when console window is scrolled
109
+ level: (config.loglevel.console && arrValidLevel.includes(config.loglevel.console.toLowerCase())) ? config.loglevel.console : 'debug',
110
+ handleExceptions: true,
111
+ stderrLevels: ['error'],
112
+ format: (config.colorize) ? consoleFormat : Log.prototype.unColorize()
113
+ }),
114
+ new winston.transports.File({
115
+ level: (config.loglevel.file && arrValidLevel.includes(config.loglevel.file.toLowerCase())) ? config.loglevel.file : 'debug',
116
+ filename: logfile,
117
+ handleExceptions: true,
118
+ format: fileFormat,
119
+ maxsize: 1024 * 1024 * 20, // 20 MB
120
+ maxFiles: 5
121
+ })
122
+ ],
123
+ exitOnError: false
119
124
  })
120
- ],
121
- exitOnError: false
122
- })
123
-
124
- // flush to disk before exit (process.exit in main code will terminate logger and we may have unflushed logfile updates)
125
- // note, still asynchronous (using exception is an alternative that gives synchronous flush and program exit)
126
- Log.prototype.exitAfterFlush = (code) => {
127
- Log.prototype.logger.transports[0].on('flush', function () {
128
- process.exit(code)
125
+ }
126
+ return winston.createLogger({
127
+ format: winston.format.combine(
128
+ maskSecret()
129
+ ),
130
+ transports: [
131
+ new winston.transports.Console({
132
+ level: (config.loglevel.console && arrValidLevel.includes(config.loglevel.console.toLowerCase())) ? config.loglevel.console : 'debug',
133
+ handleExceptions: true,
134
+ stderrLevels: ['error'],
135
+ format: (config.colorize) ? consoleFormat : Log.prototype.unColorize()
136
+ })
137
+ ],
138
+ exitOnError: false
129
139
  })
130
140
  }
131
141
 
132
- const logger = Log.prototype.logger // fix multiple loggers using stream
142
+ const logger = Log.prototype.logger() // fix multiple loggers using stream
133
143
  Log.prototype.stream = {
134
144
  write: function (message, encoding) {
135
145
  logger.info(message)
@@ -46,8 +46,8 @@ const ScimGateway = function () {
46
46
  const gwName = path.basename(__filename, '.js') // prefix of current file
47
47
  const logDir = path.join(path.dirname(requester), '..', 'logs')
48
48
  const Log = require('../lib/logger').Log
49
- const log = new Log(utils.extendObj(utils.copyObj(config.log), { category: pluginName, colorize: process.stdout.isTTY || false, loglevel: { file: 'debug', console: 'debug' } }), path.join(`${logDir}`, `${pluginName}.log`))
50
- const logger = log.logger
49
+ const log = new Log(utils.extendObj(utils.copyObj(config.log), { category: pluginName, colorize: process.stdout.isTTY || false, loglevel: { file: (!config.log.loglevel.file || config.log.loglevel.file === 'off') ? null : 'debug', console: 'debug' } }), path.join(`${logDir}`, `${pluginName}.log`))
50
+ const logger = log.logger()
51
51
  this.logger = logger // exposed to plugin-code
52
52
  this.notValidAttributes = notValidAttributes // exposed to plugin-code
53
53
  let pwErrCount = 0
@@ -228,10 +228,12 @@ const ScimGateway = function () {
228
228
  logger.error(`${gwName}[${pluginName}] stopping...\n`)
229
229
  throw (new Error('Using exception to stop further asynchronous code execution (ensure synchronous logger flush to logfile and exit program), please ignore this one...'))
230
230
  }
231
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir)
232
- if (!fs.existsSync(configDir + '/wsdls')) fs.mkdirSync(configDir + '/wsdls')
233
- if (!fs.existsSync(configDir + '/certs')) fs.mkdirSync(configDir + '/certs')
234
- if (!fs.existsSync(configDir + '/schemas')) fs.mkdirSync(configDir + '/schemas')
231
+
232
+ try {
233
+ if (!fs.existsSync(configDir + '/wsdls')) fs.mkdirSync(configDir + '/wsdls')
234
+ if (!fs.existsSync(configDir + '/certs')) fs.mkdirSync(configDir + '/certs')
235
+ if (!fs.existsSync(configDir + '/schemas')) fs.mkdirSync(configDir + '/schemas')
236
+ } catch (err) {}
235
237
 
236
238
  let isScimv2 = false
237
239
  if (config.scim.version === '2.0' || config.scim.version === 2) {
@@ -450,9 +452,9 @@ const ScimGateway = function () {
450
452
  if (tokenObj.readOnly === true && method !== 'GET') return reject(new Error('only allowing readOnly for this bearerOAuth according to bearerOAuth configuration readOnly=true'))
451
453
  return resolve(true)
452
454
  } else {
453
- for (let i = 0; i < arr.length; i++) { // resolve if token memory store have been cleared becuase of a gateway restart
454
- if (utils.getEncrypted(authToken, arr[i].client_secret) === 'token' && !arr[i].isTokenRequested) {
455
- arr[i].isTokenRequested = true // flagged as true to not allow repeated resolvements becuase token will also be cleared when expired
455
+ for (let i = 0; i < arr.length; i++) { // resolve if token memory store have been cleared because of a gateway restart
456
+ if (utils.getEncrypted(authToken, arr[i].client_secret) === arr[i].client_secret && !arr[i].isTokenRequested) {
457
+ arr[i].isTokenRequested = true // flagged as true to not allow repeated resolvements because token will also be cleared when expired
456
458
  const baseEntities = utils.copyObj(arr[i].baseEntities)
457
459
  let expires
458
460
  let readOnly = false
@@ -658,7 +660,7 @@ const ScimGateway = function () {
658
660
  for (let i = 0; i < arr.length; i++) {
659
661
  if (!arr[i].client_id || !arr[i].client_secret) continue
660
662
  if (arr[i].client_id === jsonBody.client_id && arr[i].client_secret === jsonBody.client_secret) { // authentication OK
661
- token = utils.getEncrypted('token', jsonBody.client_secret) // client_secret as seed
663
+ token = utils.getEncrypted(jsonBody.client_secret, jsonBody.client_secret)
662
664
  baseEntities = utils.copyObj(arr[i].baseEntities)
663
665
  if (arr[i].readOnly && arr[i].readOnly === true) readOnly = true
664
666
  if (arr[i].expires_in && !isNaN(arr[i].expires_in)) expires = arr[i].expires_in
@@ -1327,7 +1329,7 @@ const ScimGateway = function () {
1327
1329
  else { // try to get current groups the standard way
1328
1330
  let res
1329
1331
  try {
1330
- res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }, ['members.value', 'id', 'displayName']) // await scimgateway.getUserGroups(baseEntity, userObj.id, 'members.value,displayName')
1332
+ res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: decodeURIComponent(id) }, ['id', 'displayName'])
1331
1333
  } catch (err) {} // method may be implemented but throwing error like groups not supported/implemented
1332
1334
  currentGroups = []
1333
1335
  if (res && res.Resources && Array.isArray(res.Resources) && res.Resources.length > 0) {
@@ -1421,7 +1423,7 @@ const ScimGateway = function () {
1421
1423
  // include groups
1422
1424
  if (handle.getMethod === handler.users.getMethod && typeof this[handler.groups.getMethod] === 'function') {
1423
1425
  logger.debug(`${gwName}[${pluginName}] calling "${handler.groups.getMethod}" and awaiting result`)
1424
- const res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: id }, ['members.value', 'id', 'displayName'])
1426
+ const res = await this[handler.groups.getMethod](ctx.params.baseEntity, { attribute: 'members.value', operator: 'eq', value: id }, ['id', 'displayName'])
1425
1427
  let grps = []
1426
1428
  if (res && res.Resources && Array.isArray(res.Resources)) grps = res.Resources
1427
1429
  else if (Array.isArray(res)) grps = res
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "4.1.11",
3
+ "version": "4.1.13",
4
4
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
5
5
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",
6
6
  "homepage": "https://elshaug.xyz",
@@ -37,18 +37,18 @@
37
37
  "https-proxy-agent": "^5.0.1",
38
38
  "is-in-subnet": "^4.0.1",
39
39
  "jsonwebtoken": "^8.5.1",
40
- "koa": "^2.13.4",
40
+ "koa": "^2.14.0",
41
41
  "koa-bodyparser": "^4.3.0",
42
42
  "koa-router": "^12.0.0",
43
43
  "ldapjs": "^2.3.3",
44
44
  "lokijs": "^1.5.12",
45
- "mongodb": "^4.10.0",
45
+ "mongodb": "^4.12.1",
46
46
  "node-machine-id": "1.1.9",
47
- "nodemailer": "^6.7.8",
47
+ "nodemailer": "^6.8.0",
48
48
  "passport": "^0.6.0",
49
49
  "passport-azure-ad": "^4.3.4",
50
50
  "soap": "^0.45.0",
51
- "tedious": "^15.1.0",
51
+ "tedious": "^15.1.2",
52
52
  "winston": "^3.8.2"
53
53
  },
54
54
  "devDependencies": {