scimgateway 5.0.6 → 5.0.7

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
@@ -13,14 +13,14 @@ Validated through IdP's:
13
13
  - Okta
14
14
  - Omada
15
15
  - SailPoint/IdentityNow
16
-
16
+
17
17
  Latest news:
18
18
 
19
- - Major version **v5** marks a shift to native TypeScript support and prioritizes [Bun](https://bun.sh/) over Node.js. This upgrade requires some modifications to existing plugins.
19
+ - Major version **v5.0.0** marks a shift to native TypeScript support and prioritizes [Bun](https://bun.sh/) over Node.js. This upgrade requires some modifications to existing plugins.
20
20
  - **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 Gateway now offers enhanced functionality with support for message subscription and automated provisioning using SCIM Stream
21
21
  - Authentication PassThrough letting plugin pass authentication directly to endpoint for avoid maintaining secrets at the gateway
22
22
  - Supports OAuth Client Credentials authentication
23
- - Major version **v4** getUsers() and getGroups() replacing some deprecated methods. No limitations on filtering/sorting. Admin user access can be linked to specific baseEntities. New MongoDB plugin
23
+ - 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
24
24
  - ipAllowList for restricting access to allowlisted IP addresses or subnets e.g. Azure IP-range
25
25
  - General LDAP plugin configured for Active Directory
26
26
  - [PlugSSO](https://elshaug.xyz/docs/plugsso) using SCIM Gateway
@@ -460,6 +460,21 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
460
460
  - **email.emailOnError.cc** - Optional comma separated list of cc mail addresses
461
461
  - **email.emailOnError.subject** - Optional mail subject, default `SCIM Gateway error message`
462
462
 
463
+ Configuration notes when using default configuration oauth and tenantIdGUID - Microsoft Exchange Online (ExO):
464
+
465
+ - Entra ID application must have application permissions "**Mail.Send**"
466
+ - To prevent the sending of emails from any defined mailboxes, an ExO **ApplicationAccessPolicy** must be defined through PowerShell.
467
+
468
+ First create a mail-enabled security-group that only includes those users (mailboxes) the application is allowed to send from
469
+ Note, "mail enabled security" group cannot be created from portal, only from admin or admin.exchange console
470
+
471
+ ##Connect to Exchange
472
+ Install-Module -Name ExchangeOnlineManagement
473
+ Connect-ExchangeOnline
474
+
475
+ ##Create ApplicationAccessPolicy
476
+ New-ApplicationAccessPolicy -AppId $AppClientID -PolicyScopeGroupId $MailEnabledSecurityGrpId -AccessRight RestrictAccess -Description "Restrict app to specific mailboxes"
477
+
463
478
  - **stream** - See [SCIM Stream](https://elshaug.xyz/docs/scim-stream) for configuration details
464
479
 
465
480
  - **endpoint** - Contains endpoint specific configuration according to our **plugin code**.
@@ -589,8 +604,7 @@ docker-compose**
589
604
  **Dockerfile** <== Main dockerfile
590
605
  **DataDockerfile** <== Handles volume mapping
591
606
  **docker-compose-debug.yml** <== Debugging
592
-
593
-
607
+ **docker-compose-mssql.yml** <== Example including MSSQL docker image
594
608
 
595
609
  - Create a scimgateway user on your Linux VM.
596
610
 
@@ -1097,14 +1111,92 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1097
1111
 
1098
1112
  ## Change log
1099
1113
 
1100
- ### v5.0.6
1114
+ ### v5.0.7
1115
+
1116
+ [Improved]
1117
+
1118
+ - plugin-mssql all methods now implemented, also includes docker and dbinit configuration, **thanks to [@Peter Havekes](https://github.com/phavekes) and [@mrvanes](https://github.com/mrvanes)**
1119
+
1120
+ [Fixed]
1121
+
1122
+ - mail sending option introduced in v5.0.6 did not fully support national special charcters when using Microsoft Exchange Online and html formatted email
1123
+
1124
+ ### v5.0.6
1101
1125
 
1102
1126
  [Improved]
1103
1127
 
1104
1128
  - new configuration option: `scimgateway.idleTimeout` default 120, sets the the number of seconds to wait before timing out a connection due to inactivity
1105
- - new configuration option: `scimgateway.email` replacing legacy `scimgateway.emailOnError` (legacy still supported). Email now support oauth authentication configuration which is default and recommended for Microsoft Exchange Online.
1106
- - removed configuration option: `scimgateway.payloadSize` Bun using default maxRequestBodySize 128MB
1107
- - plugin may send email using method scimgateway.sendMail()
1129
+ - deprecated configuration option: `scimgateway.payloadSize` Bun using default maxRequestBodySize 128MB
1130
+ - new configuration option: `scimgateway.email` replacing legacy `scimgateway.emailOnError` (legacy still supported). Email now support oauth authentication
1131
+
1132
+ **old configuration:**
1133
+
1134
+ {
1135
+ "scimgateway": {
1136
+ ...
1137
+ "emailOnError": {
1138
+ "smtp": {
1139
+ "enabled": false,
1140
+ "host": null,
1141
+ "port": 587,
1142
+ "proxy": null,
1143
+ "authenticate": true,
1144
+ "username": null,
1145
+ "password": null,
1146
+ "sendInterval": 15,
1147
+ "to": null,
1148
+ "cc": null
1149
+ }
1150
+ },
1151
+ ...
1152
+ },
1153
+ ...
1154
+ }
1155
+
1156
+
1157
+ **new configuration:**
1158
+ Using Microsoft Exchange Online and oauth authencation which also is default and recommended by Microsoft
1159
+ For other mail servers and options like SMTP AUTH (basic/oauth), please see configuration description
1160
+ Plugin may also send mail using method scimgateway.sendMail()
1161
+
1162
+ {
1163
+ "scimgateway": {
1164
+ ...
1165
+ "email": {
1166
+ "auth": {
1167
+ "type": "oauth",
1168
+ "options": {
1169
+ "tenantIdGUID": null,
1170
+ "clientId": null,
1171
+ "clientSecret": null
1172
+ }
1173
+ },
1174
+ "emailOnError": {
1175
+ "enabled": false,
1176
+ "from": null,
1177
+ "to": null
1178
+ }
1179
+ },
1180
+ ...
1181
+ },
1182
+ ...
1183
+ }
1184
+
1185
+ Configuration notes when using oauth and tenantIdGUID - Microsoft Exchange Online (ExO):
1186
+
1187
+ - Entra ID application must have application permissions "**Mail.Send**"
1188
+ - To prevent the sending of emails from any defined mailboxes, an ExO **ApplicationAccessPolicy** must be defined through PowerShell.
1189
+
1190
+ First create a mail-enabled security-group that only includes those users (mailboxes) the application is allowed to send from
1191
+ Note, "mail enabled security" group cannot be created from portal, only from admin or admin.exchange console
1192
+
1193
+ ##Connect to Exchange
1194
+ Install-Module -Name ExchangeOnlineManagement
1195
+ Connect-ExchangeOnline
1196
+
1197
+ ##Create ApplicationAccessPolicy
1198
+ New-ApplicationAccessPolicy -AppId $AppClientID -PolicyScopeGroupId $MailEnabledSecurityGrpId -AccessRight RestrictAccess -Description "Restrict app to specific mailboxes"
1199
+
1108
1200
 
1109
1201
  ### v5.0.5
1110
1202
 
@@ -1573,7 +1665,7 @@ Note, obsolete - see v4.2.15 comments
1573
1665
  "forceExitTimeout": 1000
1574
1666
  }
1575
1667
 
1576
- **Thanks to Kevin Osborn**
1668
+ **Thanks to [@Kevin Osborn](https://github.com/osbornk)**
1577
1669
 
1578
1670
  ### v4.1.15
1579
1671
 
@@ -1604,7 +1696,7 @@ Note, obsolete - see v4.2.15 comments
1604
1696
  scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx)
1605
1697
  // tip, see provided example plugins
1606
1698
 
1607
- **Thanks to Kevin Osborn**
1699
+ **Thanks to [@Kevin Osborn](https://github.com/osbornk)**
1608
1700
 
1609
1701
  ### v4.1.14
1610
1702
 
@@ -1629,7 +1721,7 @@ Note, obsolete - see v4.2.15 comments
1629
1721
  [Improved]
1630
1722
 
1631
1723
  - new plugin configuration `payloadSize`. If not defined, default "1mb" will be used. There are cases which large groups could exceed default size and you may want to increase by setting your own size e.g. "5mb"
1632
- **Thanks to Sam Murphy**
1724
+ **Thanks to [@Sam Murphy*](https://github.com/SamMurphyDev)**
1633
1725
 
1634
1726
  [Fixed]
1635
1727
 
@@ -1775,7 +1867,7 @@ SCIM Gateway related news:
1775
1867
  }
1776
1868
 
1777
1869
  - postinstall copying example plugins may be skipped by setting the property `scimgateway_postinstall_skip = true` in `.npmrc` or by setting environment `SCIMGATEWAY_POSTINSTALL_SKIP = true`
1778
- - Secrets now also support key-value storage. The key defined in plugin configuration have syntax `process.text.<path>` where `<path>` is the file which contains raw (UTF-8) character value. E.g. configuration `endpoint.password` could have value `process.text./var/run/vault/endpoint.password`, and the corresponding file contains the secret. **Thanks to Raymond Augé**
1870
+ - Secrets now also support key-value storage. The key defined in plugin configuration have syntax `process.text.<path>` where `<path>` is the file which contains raw (UTF-8) character value. E.g. configuration `endpoint.password` could have value `process.text./var/run/vault/endpoint.password`, and the corresponding file contains the secret. **Thanks to [@Raymond Augé](https://github.com/rotty3000)**
1779
1871
 
1780
1872
 
1781
1873
  ### v4.0.0
@@ -1785,7 +1877,7 @@ SCIM Gateway related news:
1785
1877
  - New `getGroups()` replacing deprecated exploreGroups(), getGroup() and getGroupMembers()
1786
1878
  - Fully filter and sort support
1787
1879
  - Authentication configuration may now include a baseEntities array containing one or more `baseEntity` allowed for corresponding admin user
1788
- - New plugin-mongodb, **thanks to Filipe Ribeiro and Miguel Ferreira (KEEP SOLUTIONS)**
1880
+ - New plugin-mongodb, **Thanks to [@Filipe Ribeiro](https://github.com/fribeiro-keeps) and [@Miguel Ferreira](https://github.com/jmaferreira) (KEEP SOLUTIONS)**
1789
1881
 
1790
1882
  Note, using this major version **require existing custom plugins to be upgraded**. If you do not want to upgrade your custom plugins, the old version have to be installed using: `npm install scimgateway@3.2.11`
1791
1883
 
@@ -1893,7 +1985,7 @@ We also need to add logic from existing getGroup() and getGroupMembers()
1893
1985
  [Fixed]
1894
1986
 
1895
1987
  - Return 500 on GET handler error instead of 404
1896
- **Thanks to Nipun Dayanath**
1988
+ **Thanks to [@Nipun Dayanath](https://github.com/nipund)**
1897
1989
  - createUser/createRole response now includes id retrieved by getUser/getRole instead of using posted userName/displayName value
1898
1990
 
1899
1991
  ### v3.2.6
@@ -2373,7 +2465,7 @@ Custom plugins needs some changes (please see included example plugins)
2373
2465
 
2374
2466
  - Some minor compliance fixes
2375
2467
 
2376
- **Thanks to ywchuang**
2468
+ **Thanks to [@ywchuang](https://github.com/ywchuang)**
2377
2469
 
2378
2470
  ### v1.0.4
2379
2471
  [Improved]
@@ -2473,7 +2565,7 @@ With:
2473
2565
 
2474
2566
  - Document updated on how to run SCIM Gateway as a Docker container
2475
2567
  - `config\docker` includes docker configuration examples
2476
- **Thanks to Charley Watson and Jeffrey Gilbert**
2568
+ **Thanks to [@cwatsonc](https://github.com/cwatsonc) and [@visualjeff](https://github.com/visualjeff)**
2477
2569
 
2478
2570
 
2479
2571
  ### v0.4.5
@@ -2494,7 +2586,7 @@ With:
2494
2586
 
2495
2587
  - NoSQL Document-Oriented Database plugin: `plugin-loki`
2496
2588
  This plugin now replace previous `plugin-testmode`
2497
- **Thanks to Jeffrey Gilbert**
2589
+ **Thanks to [@visualjeff](https://github.com/visualjeff)**
2498
2590
  - Minor code/comment reorganizations in provided plugins
2499
2591
  - Minor adjustments to multi-value logic introduced in v0.4.0
2500
2592
 
@@ -2513,7 +2605,7 @@ This plugin now replace previous `plugin-testmode`
2513
2605
 
2514
2606
  - Mocha test scripts for automated testing of plugin-testmode
2515
2607
  - Automated tests run on Travis-ci.org (click on build badge)
2516
- - **Thanks to Jeffrey Gilbert**
2608
+ - **Thanks to [@visualjeff](https://github.com/visualjeff)**
2517
2609
 
2518
2610
 
2519
2611
 
@@ -0,0 +1,43 @@
1
+ USE [master];
2
+ GO
3
+
4
+ IF NOT EXISTS (SELECT * FROM sys.sql_logins WHERE name = 'scimgateway')
5
+ BEGIN
6
+ CREATE LOGIN [scimgateway] WITH PASSWORD = 'password', CHECK_POLICY = OFF;
7
+ ALTER SERVER ROLE [sysadmin] ADD MEMBER [scimgateway];
8
+ END
9
+ GO
10
+
11
+ IF DB_ID('scimgateway') IS NULL
12
+ BEGIN
13
+ CREATE DATABASE [scimgateway];
14
+ END
15
+ GO
16
+
17
+ IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'User')
18
+ BEGIN
19
+ USE [scimgateway]
20
+ CREATE TABLE [User] (
21
+ [UserID] VARCHAR(50) NOT NULL,
22
+ [Enabled] VARCHAR(50) NULL,
23
+ [Password] VARCHAR(50) NULL,
24
+ [FirstName] VARCHAR(50) NULL,
25
+ [MiddleName] VARCHAR(50) NULL,
26
+ [LastName] VARCHAR(50) NULL,
27
+ [Email] VARCHAR(50) NULL,
28
+ [MobilePhone] VARCHAR(50) NULL,
29
+ CONSTRAINT [PK_User] PRIMARY KEY ([UserID])
30
+ );
31
+ END
32
+ GO
33
+
34
+ IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Group')
35
+ BEGIN
36
+ USE [scimgateway]
37
+ CREATE TABLE [Group] (
38
+ [GroupID] VARCHAR(50) NOT NULL,
39
+ [Enabled] VARCHAR(50) NULL,
40
+ CONSTRAINT [PK_Group] PRIMARY KEY ([GroupID])
41
+ );
42
+ END
43
+ GO
@@ -0,0 +1,58 @@
1
+ version: '2'
2
+ services:
3
+ # scimgateway:
4
+ # build:
5
+ # context: .
6
+ # dockerfile: ./Dockerfile
7
+ # image: scimgateway:latest
8
+ # container_name: scimgateway
9
+ # depends_on:
10
+ # scimgateway-sqlserver:
11
+ # condition: service_healthy
12
+ # hostname:
13
+ # scimgateway
14
+ # volumes:
15
+ # - ./config:/home/scimgateway/config:rw
16
+ # - /var/lib/dbus:/var/lib/dbus:ro
17
+ # ports:
18
+ # - "8880:8880"
19
+ # # environment:
20
+ # # - NODE_ENV=production
21
+ # # - PORT=8880
22
+ # # - SEED=changeit
23
+ # restart: on-failure:3
24
+
25
+ scimgateway-sqlserver:
26
+ image: mcr.microsoft.com/mssql/server:2019-latest
27
+ hostname:
28
+ MySqlHost
29
+ environment:
30
+ - ACCEPT_EULA=Y
31
+ - SA_PASSWORD=p@ssw0rd!
32
+ - MSSQL_PID=Developer
33
+ ports:
34
+ - 1433:1433
35
+ volumes:
36
+ - ./sqlserver_data:/var/opt/mssql
37
+ user: root
38
+ restart: always
39
+ healthcheck:
40
+ test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P \"p@ssw0rd!\" -Q 'SELECT 1' || exit 1"]
41
+ interval: 10s
42
+ retries: 10
43
+ start_period: 10s
44
+ timeout: 3s
45
+
46
+ scimgateway-sqlserver-configurator:
47
+ image: mcr.microsoft.com/mssql/server:2019-latest
48
+ volumes:
49
+ - ./dbinit:/docker-entrypoint-initdb.d
50
+ depends_on:
51
+ scimgateway-sqlserver:
52
+ condition: service_healthy
53
+ restart: no
54
+ command: >
55
+ bash -c '
56
+ /opt/mssql-tools18/bin/sqlcmd -C -S MySqlHost -U sa -P "p@ssw0rd!" -d master -i docker-entrypoint-initdb.d/init.sql;
57
+ echo "All done!";
58
+ '
@@ -297,7 +297,7 @@ export class HelperRest {
297
297
  } catch (err: any) {
298
298
  throw new Error(`tls configuration error: ${err.message}`)
299
299
  }
300
- if (Object.prototype.hasOwnProperty.call(connOpt?.tls, 'rejectUnauthorized')) {
300
+ if (connOpt.tls && Object.prototype.hasOwnProperty.call(connOpt.tls, 'rejectUnauthorized')) {
301
301
  if (connOpt.tls.rejectUnauthorized !== false && connOpt.tls.rejectUnauthorized !== true) {
302
302
  delete connOpt.tls.rejectUnauthorized
303
303
  }
@@ -405,13 +405,18 @@ export class HelperRest {
405
405
  let dataString = ''
406
406
  if (body) {
407
407
  if (options.headers['Content-Type']) {
408
- if (options.headers['Content-Type'].toLowerCase() === 'application/x-www-form-urlencoded') {
408
+ const type: string = options.headers['Content-Type'].toLowerCase().trim()
409
+ if (type.startsWith('application/x-www-form-urlencoded')) {
409
410
  if (typeof body === 'string') dataString = body
410
411
  else dataString = querystring.stringify(body) // JSON to query string syntax + URL encoded
411
- } else dataString = JSON.stringify(body)
412
+ } else {
413
+ if (typeof body === 'string') dataString = body
414
+ else dataString = JSON.stringify(body)
415
+ }
412
416
  } else {
413
417
  options.headers['Content-Type'] = 'application/json; charset=utf-8'
414
- dataString = JSON.stringify(body)
418
+ if (typeof body === 'string') dataString = body
419
+ else dataString = JSON.stringify(body)
415
420
  }
416
421
  options.headers['Content-Length'] = Buffer.byteLength(dataString, 'utf8')
417
422
  options.body = dataString
@@ -6,16 +6,40 @@
6
6
  // Purpose: SQL user-provisioning
7
7
  //
8
8
  // Prereq:
9
- // TABLE [dbo].[User](
10
- // [UserID] [varchar](50) NOT NULL,
11
- // [Enabled] [varchar](50) NULL,
12
- // [Password] [varchar](50) NULL,
13
- // [FirstName] [varchar](50) NULL,
14
- // [MiddleName] [varchar](50) NULL,
15
- // [LastName] [varchar](50) NULL,
16
- // [Email] [varchar](50) NULL,
17
- // [MobilePhone] [varchar](50) NULL
18
- // )
9
+ // CREATE TABLE [User] (
10
+ // [UserID] VARCHAR(50) NOT NULL,
11
+ // [Enabled] VARCHAR(50) NULL,
12
+ // [Password] VARCHAR(50) NULL,
13
+ // [FirstName] VARCHAR(50) NULL,
14
+ // [MiddleName] VARCHAR(50) NULL,
15
+ // [LastName] VARCHAR(50) NULL,
16
+ // [Email] VARCHAR(50) NULL,
17
+ // [MobilePhone] VARCHAR(50) NULL,
18
+ // CONSTRAINT [PK_User]
19
+ // PRIMARY KEY ([UserID])
20
+ // );
21
+ //
22
+ // CREATE TABLE [Group] (
23
+ // [GroupID] VARCHAR(50) NOT NULL,
24
+ // [Enabled] VARCHAR(50) NULL,
25
+ // CONSTRAINT [PK_Group]
26
+ // PRIMARY KEY ([GroupID])
27
+ // );
28
+ //
29
+ // CREATE TABLE [Users2Group] (
30
+ // [GroupID] VARCHAR(50) NOT NULL,
31
+ // [UserID] VARCHAR(50) NOT NULL,
32
+ // CONSTRAINT [PK_Users2Group]
33
+ // PRIMARY KEY ([GroupID],[UserID]),
34
+ // CONSTRAINT [FK_U2G_Group]
35
+ // FOREIGN KEY ([GroupID])
36
+ // REFERENCES [Group]([GroupID])
37
+ // ON DELETE CASCADE,
38
+ // CONSTRAINT [FK_U2G_Users]
39
+ // FOREIGN KEY ([UserID])
40
+ // REFERENCES [User]([UserID])
41
+ // ON DELETE CASCADE
42
+ // );
19
43
  //
20
44
  // Supported attributes:
21
45
  //
@@ -69,7 +93,7 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
69
93
  if (getObj.operator) {
70
94
  if (getObj.operator === 'eq' && ['id', 'userName', 'externalId'].includes(getObj.attribute)) {
71
95
  // mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
72
- sqlQuery = `select * from [User] where UserID = '${getObj.value}'`
96
+ sqlQuery = `select * from [Users] where UserID = '${getObj.value}'`
73
97
  } else if (getObj.operator === 'eq' && getObj.attribute === 'group.value') {
74
98
  // optional - only used when groups are member of users, not default behavior - correspond to getGroupUsers() in versions < 4.x.x
75
99
  throw new Error(`${action} error: not supporting groups member of user filtering: ${getObj.rawFilter}`)
@@ -82,64 +106,41 @@ scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx) => {
82
106
  throw new Error(`${action} not error: supporting advanced filtering: ${getObj.rawFilter}`)
83
107
  } else {
84
108
  // mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all users to be returned - correspond to exploreUsers() in versions < 4.x.x
85
- sqlQuery = 'select * from [User]'
109
+ sqlQuery = 'select * from [Users]'
86
110
  }
87
111
  // mandatory if-else logic - end
88
112
 
89
113
  if (!sqlQuery) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
90
114
 
91
115
  try {
92
- return await new Promise((resolve, reject) => {
116
+ return await new Promise(async (resolve, reject) => {
93
117
  const ret: any = { // itemsPerPage will be set by scimgateway
94
118
  Resources: [],
95
119
  totalResults: null,
96
120
  }
97
121
 
98
- const connectionCfg: any = scimgateway.copyObj(config.connection)
99
- if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
100
- if (!connectionCfg.authentication) connectionCfg.authentication = {}
101
- if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
102
- if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
103
- const [username, password] = getCtxAuth(ctx)
104
- connectionCfg.authentication.options.password = password
105
- if (username) connectionCfg.authentication.options.userName = username
106
- }
107
-
108
- const connection = new Connection(connectionCfg)
109
-
110
- connection.on('connect', function (err) {
111
- if (err) {
112
- const e = new Error(`exploreUsers MSSQL client connect error: ${err.message}`)
113
- return reject(e)
122
+ const users: any[] = await query(sqlQuery, ctx).catch((err: any) => {
123
+ const e = new Error(`${action} error: ${err.message}`)
124
+ return reject(e)
125
+ })
126
+
127
+ for (const user of users) {
128
+ const scimUser = {
129
+ id: user.UserID.value ? user.UserID.value : undefined,
130
+ userName: user.UserID.value ? user.UserID.value : undefined,
131
+ active: user.Enabled.value === 'true' || false,
132
+ name: {
133
+ givenName: user.FirstName.value ? user.FirstName.value : undefined,
134
+ middleName: user.MiddleName.value ? user.MiddleName.value : undefined,
135
+ familyName: user.LastName.value ? user.LastName.value : undefined,
136
+ },
137
+ phoneNumbers: user.MobilePhone.value ? [{ type: 'work', value: user.MobilePhone.value }] : undefined,
138
+ emails: user.Email.value ? [{ type: 'work', value: user.Email.value }] : undefined,
114
139
  }
115
- const request = new Request(sqlQuery, function (err, rowCount, rows) {
116
- if (err) {
117
- connection.close()
118
- const e = new Error(`exploreUsers MSSQL client request: ${sqlQuery} Error: ${err.message}`)
119
- return reject(e)
120
- }
140
+ ret.Resources.push(scimUser)
141
+ }
121
142
 
122
- for (const row in rows) {
123
- const scimUser = {
124
- id: rows[row].UserID.value ? rows[row].UserID.value : undefined,
125
- userName: rows[row].UserID.value ? rows[row].UserID.value : undefined,
126
- active: rows[row].Enabled.value === 'true' || false,
127
- name: {
128
- givenName: rows[row].FirstName.value ? rows[row].FirstName.value : undefined,
129
- middleName: rows[row].MiddleName.value ? rows[row].MiddleName.value : undefined,
130
- familyName: rows[row].LastName.value ? rows[row].LastName.value : undefined,
131
- },
132
- phoneNumbers: rows[row].MobilePhone.value ? [{ type: 'work', value: rows[row].MobilePhone.value }] : undefined,
133
- emails: rows[row].Email.value ? [{ type: 'work', value: rows[row].Email.value }] : undefined,
134
- }
135
- ret.Resources.push(scimUser)
136
- }
137
- connection.close()
138
- resolve(ret) // all explored users
139
- }) // request
140
- connection.execSql(request)
141
- }) // connection
142
- connection.connect() // initialize the connection
143
+ resolve(ret) // all explored users
143
144
  }) // Promise
144
145
  } catch (err: any) {
145
146
  throw new Error(`${action} error: ${err.message}`)
@@ -154,7 +155,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
154
155
  scimgateway.logDebug(baseEntity, `handling "${action}" userObj=${JSON.stringify(userObj)}`)
155
156
 
156
157
  try {
157
- return await new Promise((resolve, reject) => {
158
+ return await new Promise(async (resolve, reject) => {
158
159
  if (!userObj.name) userObj.name = {}
159
160
  if (!userObj.emails) userObj.emails = { work: {} }
160
161
  if (!userObj.phoneNumbers) userObj.phoneNumbers = { work: {} }
@@ -170,37 +171,15 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
170
171
  Email: (userObj.emails.work.value) ? `'${userObj.emails.work.value}'` : null,
171
172
  }
172
173
 
173
- const connectionCfg: any = scimgateway.copyObj(config.connection)
174
- if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
175
- if (!connectionCfg.authentication) connectionCfg.authentication = {}
176
- if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
177
- if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
178
- const [username, password] = getCtxAuth(ctx)
179
- connectionCfg.authentication.options.password = password
180
- if (username) connectionCfg.authentication.options.userName = username
181
- }
182
- const connection = new Connection(connectionCfg)
183
-
184
- connection.on('connect', function (err) {
185
- if (err) {
186
- const e = new Error(`createUser MSSQL client connect error: ${err.message}`)
187
- return reject(e)
188
- }
189
- const sqlQuery = `insert into [User] (UserID, Enabled, Password, FirstName, MiddleName, LastName, Email, MobilePhone)
174
+ const sqlQuery = `insert into [Users] (UserID, Enabled, Password, FirstName, MiddleName, LastName, Email, MobilePhone)
190
175
  values (${insert.UserID}, ${insert.Enabled}, ${insert.Password}, ${insert.FirstName}, ${insert.MiddleName}, ${insert.LastName}, ${insert.Email}, ${insert.MobilePhone})`
191
176
 
192
- const request = new Request(sqlQuery, function (err) {
193
- if (err) {
194
- connection.close()
195
- const e = new Error(`createUser MSSQL client request: ${sqlQuery} error: ${err.message}`)
196
- return reject(e)
197
- }
198
- connection.close()
199
- resolve(null)
200
- }) // request
201
- connection.execSql(request)
202
- }) // connection
203
- connection.connect() // initialize the connection
177
+ await query(sqlQuery, ctx).catch((err: any) => {
178
+ const e = new Error(`${action} error: ${err.message}`)
179
+ return reject(e)
180
+ })
181
+
182
+ resolve(null)
204
183
  }) // Promise
205
184
  } catch (err: any) {
206
185
  throw new Error(`${action} error: ${err.message}`)
@@ -215,36 +194,14 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
215
194
  scimgateway.logDebug(baseEntity, `handling "${action}" id=${id}`)
216
195
 
217
196
  try {
218
- return await new Promise((resolve, reject) => {
219
- const connectionCfg: any = scimgateway.copyObj(config.connection)
220
- if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
221
- if (!connectionCfg.authentication) connectionCfg.authentication = {}
222
- if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
223
- if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
224
- const [username, password] = getCtxAuth(ctx)
225
- connectionCfg.authentication.options.password = password
226
- if (username) connectionCfg.authentication.options.userName = username
227
- }
228
- const connection = new Connection(connectionCfg)
229
-
230
- connection.on('connect', function (err) {
231
- if (err) {
232
- const e = new Error(`deleteUser MSSQL client connect error: ${err.message}`)
233
- return reject(e)
234
- }
235
- const sqlQuery = `delete from [User] where UserID = '${id}'`
236
- const request = new Request(sqlQuery, function (err) {
237
- if (err) {
238
- connection.close()
239
- const e = new Error(`deleteUser MSSQL client request: ${sqlQuery} error: ${err.message}`)
240
- return reject(e)
241
- }
242
- connection.close()
243
- resolve(null)
244
- }) // request
245
- connection.execSql(request)
246
- }) // connection
247
- connection.connect() // initialize the connection
197
+ return await new Promise(async (resolve, reject) => {
198
+ const sqlQuery = `delete from [Users] where UserID = '${id}'`
199
+ await query(sqlQuery, ctx).catch((err: any) => {
200
+ const e = new Error(`${action} error: ${err.message}`)
201
+ return reject(e)
202
+ })
203
+
204
+ resolve(null)
248
205
  }) // Promise
249
206
  } catch (err: any) {
250
207
  throw new Error(`${action} error: ${err.message}`)
@@ -259,7 +216,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
259
216
  scimgateway.logDebug(baseEntity, `handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
260
217
 
261
218
  try {
262
- return await new Promise((resolve, reject) => {
219
+ return await new Promise(async (resolve, reject) => {
263
220
  if (!attrObj.name) attrObj.name = {}
264
221
  if (!attrObj.emails) attrObj.emails = { work: {} }
265
222
  if (!attrObj.phoneNumbers) attrObj.phoneNumbers = { work: {} }
@@ -294,35 +251,13 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
294
251
 
295
252
  sql = sql.substr(0, sql.length - 1) // remove trailing ","
296
253
 
297
- const connectionCfg: any = scimgateway.copyObj(config.connection)
298
- if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
299
- if (!connectionCfg.authentication) connectionCfg.authentication = {}
300
- if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
301
- if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
302
- const [username, password] = getCtxAuth(ctx)
303
- connectionCfg.authentication.options.password = password
304
- if (username) connectionCfg.authentication.options.userName = username
305
- }
306
- const connection = new Connection(connectionCfg)
254
+ const sqlQuery = `update [Users] set ${sql} where UserID like '${id}'`
255
+ await query(sqlQuery, ctx).catch((err: any) => {
256
+ const e = new Error(`${action} error: ${err.message}`)
257
+ return reject(e)
258
+ })
307
259
 
308
- connection.on('connect', function (err) {
309
- if (err) {
310
- const e = new Error(`modifyUser MSSQL client connect error: ${err.message}`)
311
- return reject(e)
312
- }
313
- const sqlQuery = `update [User] set ${sql} where UserID like '${id}'`
314
- const request = new Request(sqlQuery, function (err) {
315
- if (err) {
316
- connection.close()
317
- const e = new Error(`modifyUser MSSQL client request: ${sqlQuery} error: ${err.message}`)
318
- return reject(e)
319
- }
320
- connection.close()
321
- resolve(null)
322
- }) // request
323
- connection.execSql(request)
324
- }) // connection
325
- connection.connect() // initialize the connection
260
+ resolve(null)
326
261
  }) // Promise
327
262
  } catch (err: any) {
328
263
  throw new Error(`${action} error: ${err.message}`)
@@ -332,55 +267,174 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
332
267
  // =================================================
333
268
  // getGroups
334
269
  // =================================================
335
- scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
270
+ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
336
271
  const action = 'getGroups'
337
272
  scimgateway.logDebug(baseEntity, `handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
338
273
 
274
+ let sqlQuery
275
+
339
276
  // mandatory if-else logic - start
340
277
  if (getObj.operator) {
341
278
  if (getObj.operator === 'eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
342
- // mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
279
+ // mandatory - unique filtering - single unique user to be returned - correspond to getGroup() in versions < 4.x.x
280
+ sqlQuery = `select * from [Groups] where GroupID = '${getObj.value}'`
343
281
  } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
344
282
  // mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
345
- // Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
283
+ sqlQuery = `select * from [Groups] join [Users2Group] on Groups.GroupID = Users2Group.GroupID where Users2Group.UserID = '${getObj.value}'`
346
284
  } else {
347
285
  // optional - simpel filtering
286
+ throw new Error(`${action} error: not supporting simple filtering: ${getObj.rawFilter}`)
348
287
  }
349
288
  } else if (getObj.rawFilter) {
350
289
  // optional - advanced filtering having and/or/not - use getObj.rawFilter
290
+ throw new Error(`${action} not error: supporting advanced filtering: ${getObj.rawFilter}`)
351
291
  } else {
352
292
  // mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all groups to be returned - correspond to exploreGroups() in versions < 4.x.x
293
+ sqlQuery = 'select * from [Groups]'
353
294
  }
354
295
  // mandatory if-else logic - end
296
+ if (!sqlQuery) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
355
297
 
356
- return { Resources: [] } // groups not supported - returning empty Resources
298
+ try {
299
+ return await new Promise(async (resolve, reject) => {
300
+ const ret: any = { // itemsPerPage will be set by scimgateway
301
+ Resources: [],
302
+ totalResults: null,
303
+ }
304
+
305
+ const groups = await query(sqlQuery, ctx).catch((err: any) => {
306
+ const e = new Error(`${action} error: ${err.message}`)
307
+ return reject(e)
308
+ })
309
+
310
+ for (const group of groups) {
311
+ const scimGroup: Record<string, any> = {
312
+ id: group.GroupID.value ? group.GroupID.value : undefined,
313
+ displayName: group.GroupID.value ? group.GroupID.value : undefined,
314
+ active: group.Enabled.value === 'true' || false,
315
+ members: [],
316
+ }
317
+
318
+ const sqlQuery = `select UserID from [Users2Group] where GroupID = '${scimGroup.id}'`
319
+ const members = await query(sqlQuery, ctx).catch(e => console.warn(`${e}`))
320
+ for (const member of members) {
321
+ const scimMember = {
322
+ value: member.UserID.value,
323
+ display: member.UserID.value,
324
+ }
325
+ scimGroup.members.push(scimMember)
326
+ }
327
+
328
+ ret.Resources.push(scimGroup)
329
+ }
330
+
331
+ resolve(ret)
332
+ }) // Promise
333
+ } catch (err: any) {
334
+ throw new Error(`${action} error: ${err.message}`)
335
+ }
357
336
  }
358
337
 
359
338
  // =================================================
360
339
  // createGroup
361
340
  // =================================================
362
- scimgateway.createGroup = async (baseEntity, groupObj) => {
341
+ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
363
342
  const action = 'createGroup'
364
343
  scimgateway.logDebug(baseEntity, `handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
365
- throw new Error(`${action} error: ${action} is not supported`)
344
+
345
+ try {
346
+ return await new Promise(async (resolve, reject) => {
347
+ const insert = {
348
+ GroupID: `'${groupObj.displayName}'`,
349
+ Enabled: (groupObj.active) ? `'${groupObj.active}'` : '\'false\'',
350
+ }
351
+
352
+ const sqlQuery = `insert into [Groups] (GroupID, Enabled) values (${insert.GroupID}, ${insert.Enabled})`
353
+ await query(sqlQuery, ctx).catch(e => console.warn(`${e}`))
354
+
355
+ for (const member of groupObj.members) {
356
+ const sqlQuery = `insert into [Users2Group] (UserID, GroupID) values ('${member.value}', ${insert.GroupID})`
357
+ await query(sqlQuery, ctx).catch((err: any) => {
358
+ const e = new Error(`${action} error: ${err.message}`)
359
+ return reject(e)
360
+ })
361
+ }
362
+
363
+ resolve(null)
364
+ }) // Promise
365
+ } catch (err: any) {
366
+ throw new Error(`${action} error: ${err.message}`)
367
+ }
366
368
  }
367
369
 
368
370
  // =================================================
369
371
  // deleteGroup
370
372
  // =================================================
371
- scimgateway.deleteGroup = async (baseEntity, id) => {
373
+ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
372
374
  const action = 'deleteGroup'
373
375
  scimgateway.logDebug(baseEntity, `handling "${action}" id=${id}`)
374
- throw new Error(`${action} error: ${action} is not supported`)
376
+
377
+ try {
378
+ return await new Promise(async (resolve, reject) => {
379
+ const sqlQuery = `delete from [Groups] where GroupID = '${id}'`
380
+ await query(sqlQuery, ctx).catch((err: any) => {
381
+ const e = new Error(`${action} error: ${err.message}`)
382
+ return reject(e)
383
+ })
384
+
385
+ resolve(null)
386
+ }) // Promise
387
+ } catch (err: any) {
388
+ throw new Error(`${action} error: ${err.message}`)
389
+ }
375
390
  }
376
391
 
377
392
  // =================================================
378
393
  // modifyGroup
379
394
  // =================================================
380
- scimgateway.modifyGroup = async (baseEntity, id, attrObj) => {
395
+ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
381
396
  const action = 'modifyGroup'
382
397
  scimgateway.logDebug(baseEntity, `handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
383
- throw new Error(`${action} error: ${action} is not supported`)
398
+
399
+ let sql = ''
400
+
401
+ if (attrObj.active !== undefined) sql += `Enabled='${attrObj.active}',`
402
+ sql = sql.substr(0, sql.length - 1) // remove trailing ","
403
+
404
+ const queries = []
405
+ if (sql) {
406
+ queries.push(`update [Groups] set ${sql} where GroupID like '${id}'`)
407
+ }
408
+
409
+ // This BLINDLY inserts all user/groups and gracefully breaks on PK violation
410
+ // for each existing membership
411
+ if (Array.isArray(attrObj.members) && attrObj.members) {
412
+ attrObj.members.forEach((member) => {
413
+ if (member.operation == 'delete') {
414
+ queries.push(`delete from [Users2Group] where GroupID='${id}' and UserID='${member.value}'`)
415
+ } else {
416
+ queries.push(`insert into [Users2Group] (UserID, GroupID) values ('${member.value}','${id}')`)
417
+ }
418
+ })
419
+ }
420
+
421
+ const sqlQuery = queries.join(';')
422
+
423
+ try {
424
+ return await new Promise(async (resolve, reject) => {
425
+ if (sqlQuery) {
426
+ scimgateway.logDebug(baseEntity, `sqlQuery: ${sqlQuery}`)
427
+ await query(sqlQuery, ctx).catch((err: any) => {
428
+ const e = new Error(`${action} error: ${err.message}`)
429
+ return reject(e)
430
+ })
431
+ }
432
+
433
+ resolve(null)
434
+ }) // Promise
435
+ } catch (err: any) {
436
+ throw new Error(`${action} error: ${err.message}`)
437
+ }
384
438
  }
385
439
 
386
440
  // =================================================
@@ -399,6 +453,42 @@ const getCtxAuth = (ctx: undefined | Record<string, any>) => {
399
453
  else return [undefined, authToken] // bearer auth
400
454
  }
401
455
 
456
+ const connectionCfg = (ctx: undefined | Record<string, any>) => {
457
+ const connectionCfg = scimgateway.copyObj(config.connection)
458
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
459
+ if (!connectionCfg.authentication) connectionCfg.authentication = {}
460
+ if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
461
+ if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
462
+ const [username, password] = getCtxAuth(ctx)
463
+ connectionCfg.authentication.options.password = password
464
+ if (username) connectionCfg.authentication.options.userName = username
465
+ }
466
+ return connectionCfg
467
+ }
468
+
469
+ const query: (sql: string, ctx: any) => Promise<any> = (sql, ctx) => new Promise((resolve, reject) => {
470
+ const connection = new Connection(connectionCfg(ctx))
471
+
472
+ connection.connect((err) => {
473
+ if (err) {
474
+ const e = new Error(`MSSQL client connect error: ${err.message}`)
475
+ reject(e)
476
+ } else {
477
+ const request = new Request(sql, (err, rowCount, rows) => {
478
+ if (err) {
479
+ connection.close()
480
+ const e = new Error(`MSSQL client request: ${sql} Error: ${err.message}`)
481
+ reject(e)
482
+ } else {
483
+ connection.close()
484
+ resolve(rows)
485
+ }
486
+ })
487
+ connection.execSql(request)
488
+ }
489
+ })
490
+ })
491
+
402
492
  //
403
493
  // Cleanup on exit
404
494
  //
@@ -2688,21 +2688,21 @@ export class ScimGateway {
2688
2688
  * logDebug logs debug message
2689
2689
  **/
2690
2690
  logDebug(baseEntity: string | undefined, msg: string) {
2691
- this.logger.debug(`${this.pluginName}[${baseEntity}]} ${msg}`)
2691
+ this.logger.debug(`${this.pluginName}[${baseEntity}] ${msg}`)
2692
2692
  }
2693
2693
 
2694
2694
  /**
2695
2695
  * logInfo logs info message
2696
2696
  **/
2697
2697
  logInfo(baseEntity: string | undefined, msg: string) {
2698
- this.logger.info(`${this.pluginName}[${baseEntity}]} ${msg}`)
2698
+ this.logger.info(`${this.pluginName}[${baseEntity}] ${msg}`)
2699
2699
  }
2700
2700
 
2701
2701
  /**
2702
2702
  * logError logs error message
2703
2703
  **/
2704
2704
  logError(baseEntity: string | undefined, msg: string) {
2705
- this.logger.error(`${this.pluginName}[${baseEntity}]} ${msg}`)
2705
+ this.logger.error(`${this.pluginName}[${baseEntity}] ${msg}`)
2706
2706
  }
2707
2707
 
2708
2708
  /**
@@ -2919,15 +2919,28 @@ export class ScimGateway {
2919
2919
  if (!this.helperRest) this.helperRest = new HelperRest(this, { entity: { undefined: { connection: this.config.scimgateway.email } } })
2920
2920
  if (this.config.scimgateway.email.auth?.options?.tenantIdGUID) {
2921
2921
  // Graph API
2922
+ let content: string
2923
+ if (isHtml) { // ExO workaround for national special caracters in html content - require singleValueExtendedProperties being used
2924
+ var enc = new TextEncoder() // utf-8
2925
+ const buf = enc.encode(msgObj.content)
2926
+ content = new TextDecoder('windows-1252').decode(buf)
2927
+ } else content = msgObj.content
2928
+
2922
2929
  const emailMessage: Record<string, any> = {
2923
2930
  message: {
2924
2931
  subject: msgObj.subject ? msgObj.subject : 'SCIM Gateway message',
2925
2932
  body: {
2926
- content: msgObj.content,
2933
+ content,
2927
2934
  contentType: isHtml ? 'HTML' : 'Text',
2928
2935
  },
2929
2936
  toRecipients: [],
2930
2937
  ccRecipients: [],
2938
+ singleValueExtendedProperties: [ // force using ExO header: Content-Type: text/plain; charset="utf-8"
2939
+ {
2940
+ id: 'Integer 0x3fde', // Content-Type header - can be verifed by checking raw mail
2941
+ value: '65001', // text/plain; charset="utf-8"
2942
+ },
2943
+ ],
2931
2944
  },
2932
2945
  saveToSentItems: 'false',
2933
2946
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.0.6",
3
+ "version": "5.0.7",
4
4
  "type": "module",
5
5
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
6
6
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",