scimgateway 5.0.6 → 5.0.8

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
@@ -258,8 +258,8 @@ Below shows an example of config\plugin-saphana.json
258
258
  ],
259
259
  "bearerOAuth": [
260
260
  {
261
- "client_id": null,
262
- "client_secret": null,
261
+ "clientId": null,
262
+ "clientSecret": null,
263
263
  "readOnly": false,
264
264
  "baseEntities": []
265
265
  }
@@ -398,7 +398,7 @@ Definitions in `endpoint` object are customized according to our plugin code. Pl
398
398
 
399
399
  - **auth.bearerJwt** - Array of one or more standard JWT objects. Using **secret** or **publicKey** for signature verification. publicKey should be set to the filename of public key or certificate pem-file located in `<package-root>\config\certs` or absolute path being used. Clear text secret will become encrypted when gateway is started. **options.issuer** is mandatory. Other options may also be included according to jsonwebtoken npm package definition.
400
400
 
401
- - **auth.bearerOAuth** - Array of one or more Client Credentials OAuth configuration objects. **`client_id`** and **`client_secret`** are mandatory. client_secret value will become encrypted when gateway is started. OAuth token request url is **/oauth/token** e.g. http://localhost:8880/oauth/token
401
+ - **auth.bearerOAuth** - Array of one or more Client Credentials OAuth configuration objects. **`clientId`** and **`clientSecret`** are mandatory. clientSecret value will become encrypted when gateway is started. OAuth token request url is **/oauth/token** e.g. http://localhost:8880/oauth/token
402
402
 
403
403
  - **auth.passThrough** - Setting **auth.passThrough.enabled=true** will bypass SCIM Gateway authentication. Gateway will instead pass ctx containing authentication header to the plugin. Plugin could then use this information for endpoint authentication and we don't have any password/token stored at the gateway. Note, this also requires plugin binary having `scimgateway.authPassThroughAllowed = true` and endpoint logic for handling/passing ctx.request.header.authorization
404
404
 
@@ -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,99 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1097
1111
 
1098
1112
  ## Change log
1099
1113
 
1100
- ### v5.0.6
1114
+ ### v5.0.8
1115
+
1116
+ [Fixed]
1117
+
1118
+ - Ensure Bun compatibility with Azure Reverse Proxy for large and long running response
1119
+ - HelperRest was not compatible with Node.js
1120
+ - plugin-mssql, some error handling should not throw an error
1121
+ - Configuration files updated according to the v5 configuration syntax of `scimgateway.auth.bearerOAuth` - `clientId/clientSecret` now replacing deprecated `client_id/client_secret`
1122
+
1123
+ ### v5.0.7
1124
+
1125
+ [Improved]
1126
+
1127
+ - 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)**
1128
+
1129
+ [Fixed]
1130
+
1131
+ - mail sending option introduced in v5.0.6 did not fully support national special charcters when using Microsoft Exchange Online and html formatted email
1132
+
1133
+ ### v5.0.6
1101
1134
 
1102
1135
  [Improved]
1103
1136
 
1104
1137
  - 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()
1138
+ - deprecated configuration option: `scimgateway.payloadSize` Bun using default maxRequestBodySize 128MB
1139
+ - new configuration option: `scimgateway.email` replacing legacy `scimgateway.emailOnError` (legacy still supported). Email now support oauth authentication
1140
+
1141
+ **old configuration:**
1142
+
1143
+ {
1144
+ "scimgateway": {
1145
+ ...
1146
+ "emailOnError": {
1147
+ "smtp": {
1148
+ "enabled": false,
1149
+ "host": null,
1150
+ "port": 587,
1151
+ "proxy": null,
1152
+ "authenticate": true,
1153
+ "username": null,
1154
+ "password": null,
1155
+ "sendInterval": 15,
1156
+ "to": null,
1157
+ "cc": null
1158
+ }
1159
+ },
1160
+ ...
1161
+ },
1162
+ ...
1163
+ }
1164
+
1165
+
1166
+ **new configuration:**
1167
+ Using Microsoft Exchange Online and oauth authencation which also is default and recommended by Microsoft. For other mail servers and options like SMTP AUTH (basic/oauth), please see configuration description. Plugin may also send mail using method scimgateway.sendMail()
1168
+
1169
+ {
1170
+ "scimgateway": {
1171
+ ...
1172
+ "email": {
1173
+ "auth": {
1174
+ "type": "oauth",
1175
+ "options": {
1176
+ "tenantIdGUID": null,
1177
+ "clientId": null,
1178
+ "clientSecret": null
1179
+ }
1180
+ },
1181
+ "emailOnError": {
1182
+ "enabled": false,
1183
+ "from": null,
1184
+ "to": null
1185
+ }
1186
+ },
1187
+ ...
1188
+ },
1189
+ ...
1190
+ }
1191
+
1192
+ Configuration notes when using oauth and tenantIdGUID - Microsoft Exchange Online (ExO):
1193
+
1194
+ - Entra ID application must have application permissions `Mail.Send`
1195
+ - To prevent the sending of emails from any defined mailboxes, an ExO `ApplicationAccessPolicy` must be defined through PowerShell.
1196
+
1197
+ First create a mail-enabled security-group that only includes those users (mailboxes) the application is allowed to send from
1198
+ Note, `mail enabled security group` cannot be created from portal, only from admin or admin.exchange console
1199
+
1200
+ ##Connect to Exchange
1201
+ Install-Module -Name ExchangeOnlineManagement
1202
+ Connect-ExchangeOnline
1203
+
1204
+ ##Create ApplicationAccessPolicy
1205
+ New-ApplicationAccessPolicy -AppId <AppClientID> -PolicyScopeGroupId <MailEnabledSecurityGrpId> -AccessRight RestrictAccess -Description "Restrict app to specific mailboxes"
1206
+
1108
1207
 
1109
1208
  ### v5.0.5
1110
1209
 
@@ -1216,7 +1315,7 @@ Besides going from JavaScript to TypeScript, following can be mentioned:
1216
1315
 
1217
1316
  * Use scimgateway.HelperRest() for REST functionlity, also supports Auth PassThrough
1218
1317
  * scimgateway.endpointMapper() may be used for inbound/outbound attribute mappings
1219
- * In general when using TypeScript, variables should be type defined: `let isDone: boolean = false`, `catch (err: any)`, ...
1318
+ * In general when using TypeScript, variables should be type-defined: `let isDone: boolean = false`, `catch (err: any)`, ...
1220
1319
 
1221
1320
  ### v4.5.12
1222
1321
 
@@ -1573,7 +1672,7 @@ Note, obsolete - see v4.2.15 comments
1573
1672
  "forceExitTimeout": 1000
1574
1673
  }
1575
1674
 
1576
- **Thanks to Kevin Osborn**
1675
+ **Thanks to [@Kevin Osborn](https://github.com/osbornk)**
1577
1676
 
1578
1677
  ### v4.1.15
1579
1678
 
@@ -1604,7 +1703,7 @@ Note, obsolete - see v4.2.15 comments
1604
1703
  scimgateway.getUsers = async (baseEntity, getObj, attributes, ctx)
1605
1704
  // tip, see provided example plugins
1606
1705
 
1607
- **Thanks to Kevin Osborn**
1706
+ **Thanks to [@Kevin Osborn](https://github.com/osbornk)**
1608
1707
 
1609
1708
  ### v4.1.14
1610
1709
 
@@ -1629,7 +1728,7 @@ Note, obsolete - see v4.2.15 comments
1629
1728
  [Improved]
1630
1729
 
1631
1730
  - 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**
1731
+ **Thanks to [@Sam Murphy*](https://github.com/SamMurphyDev)**
1633
1732
 
1634
1733
  [Fixed]
1635
1734
 
@@ -1775,7 +1874,7 @@ SCIM Gateway related news:
1775
1874
  }
1776
1875
 
1777
1876
  - 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é**
1877
+ - 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
1878
 
1780
1879
 
1781
1880
  ### v4.0.0
@@ -1785,7 +1884,7 @@ SCIM Gateway related news:
1785
1884
  - New `getGroups()` replacing deprecated exploreGroups(), getGroup() and getGroupMembers()
1786
1885
  - Fully filter and sort support
1787
1886
  - 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)**
1887
+ - New plugin-mongodb, **Thanks to [@Filipe Ribeiro](https://github.com/fribeiro-keeps) and [@Miguel Ferreira](https://github.com/jmaferreira) (KEEP SOLUTIONS)**
1789
1888
 
1790
1889
  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
1890
 
@@ -1893,7 +1992,7 @@ We also need to add logic from existing getGroup() and getGroupMembers()
1893
1992
  [Fixed]
1894
1993
 
1895
1994
  - Return 500 on GET handler error instead of 404
1896
- **Thanks to Nipun Dayanath**
1995
+ **Thanks to [@Nipun Dayanath](https://github.com/nipund)**
1897
1996
  - createUser/createRole response now includes id retrieved by getUser/getRole instead of using posted userName/displayName value
1898
1997
 
1899
1998
  ### v3.2.6
@@ -2373,7 +2472,7 @@ Custom plugins needs some changes (please see included example plugins)
2373
2472
 
2374
2473
  - Some minor compliance fixes
2375
2474
 
2376
- **Thanks to ywchuang**
2475
+ **Thanks to [@ywchuang](https://github.com/ywchuang)**
2377
2476
 
2378
2477
  ### v1.0.4
2379
2478
  [Improved]
@@ -2473,7 +2572,7 @@ With:
2473
2572
 
2474
2573
  - Document updated on how to run SCIM Gateway as a Docker container
2475
2574
  - `config\docker` includes docker configuration examples
2476
- **Thanks to Charley Watson and Jeffrey Gilbert**
2575
+ **Thanks to [@cwatsonc](https://github.com/cwatsonc) and [@visualjeff](https://github.com/visualjeff)**
2477
2576
 
2478
2577
 
2479
2578
  ### v0.4.5
@@ -2494,7 +2593,7 @@ With:
2494
2593
 
2495
2594
  - NoSQL Document-Oriented Database plugin: `plugin-loki`
2496
2595
  This plugin now replace previous `plugin-testmode`
2497
- **Thanks to Jeffrey Gilbert**
2596
+ **Thanks to [@visualjeff](https://github.com/visualjeff)**
2498
2597
  - Minor code/comment reorganizations in provided plugins
2499
2598
  - Minor adjustments to multi-value logic introduced in v0.4.0
2500
2599
 
@@ -2513,7 +2612,7 @@ This plugin now replace previous `plugin-testmode`
2513
2612
 
2514
2613
  - Mocha test scripts for automated testing of plugin-testmode
2515
2614
  - Automated tests run on Travis-ci.org (click on build badge)
2516
- - **Thanks to Jeffrey Gilbert**
2615
+ - **Thanks to [@visualjeff](https://github.com/visualjeff)**
2517
2616
 
2518
2617
 
2519
2618
 
@@ -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
+ '
@@ -52,8 +52,8 @@
52
52
  ],
53
53
  "bearerOAuth": [
54
54
  {
55
- "client_id": null,
56
- "client_secret": null,
55
+ "clientId": null,
56
+ "clientSecret": null,
57
57
  "readOnly": false,
58
58
  "baseEntities": []
59
59
  }
@@ -52,8 +52,8 @@
52
52
  ],
53
53
  "bearerOAuth": [
54
54
  {
55
- "client_id": null,
56
- "client_secret": null,
55
+ "clientId": null,
56
+ "clientSecret": null,
57
57
  "readOnly": false,
58
58
  "baseEntities": []
59
59
  }
@@ -52,8 +52,8 @@
52
52
  ],
53
53
  "bearerOAuth": [
54
54
  {
55
- "client_id": null,
56
- "client_secret": null,
55
+ "clientId": null,
56
+ "clientSecret": null,
57
57
  "readOnly": false,
58
58
  "baseEntities": []
59
59
  }
@@ -52,8 +52,8 @@
52
52
  ],
53
53
  "bearerOAuth": [
54
54
  {
55
- "client_id": null,
56
- "client_secret": null,
55
+ "clientId": null,
56
+ "clientSecret": null,
57
57
  "readOnly": false,
58
58
  "baseEntities": []
59
59
  }
@@ -58,8 +58,8 @@
58
58
  ],
59
59
  "bearerOAuth": [
60
60
  {
61
- "client_id": null,
62
- "client_secret": null,
61
+ "clientId": null,
62
+ "clientSecret": null,
63
63
  "readOnly": false,
64
64
  "baseEntities": []
65
65
  }
@@ -52,8 +52,8 @@
52
52
  ],
53
53
  "bearerOAuth": [
54
54
  {
55
- "client_id": null,
56
- "client_secret": null,
55
+ "clientId": null,
56
+ "clientSecret": null,
57
57
  "readOnly": false,
58
58
  "baseEntities": []
59
59
  }
@@ -52,8 +52,8 @@
52
52
  ],
53
53
  "bearerOAuth": [
54
54
  {
55
- "client_id": null,
56
- "client_secret": null,
55
+ "clientId": null,
56
+ "clientSecret": null,
57
57
  "readOnly": false,
58
58
  "baseEntities": []
59
59
  }
@@ -52,8 +52,8 @@
52
52
  ],
53
53
  "bearerOAuth": [
54
54
  {
55
- "client_id": null,
56
- "client_secret": null,
55
+ "clientId": null,
56
+ "clientSecret": null,
57
57
  "readOnly": false,
58
58
  "baseEntities": []
59
59
  }
@@ -52,8 +52,8 @@
52
52
  ],
53
53
  "bearerOAuth": [
54
54
  {
55
- "client_id": null,
56
- "client_secret": null,
55
+ "clientId": null,
56
+ "clientSecret": null,
57
57
  "readOnly": false,
58
58
  "baseEntities": []
59
59
  }
@@ -4,7 +4,7 @@ import { Buffer } from 'node:buffer'
4
4
  import fs from 'node:fs'
5
5
  import querystring from 'querystring'
6
6
  import * as utils from './utils.ts'
7
- import ScimGateway from 'scimgateway'
7
+ // import type { ScimGateway } from 'scimgateway' // comment out for supporting Node.js, using type any and no IntelliSense
8
8
 
9
9
  /**
10
10
  * HelperRest includes function doRequest() for doing REST calls
@@ -13,12 +13,12 @@ export class HelperRest {
13
13
  private lock = new utils.Lock()
14
14
  private _serviceClient: Record<string, any> = {}
15
15
  private config_entity: any
16
- private scimgateway: ScimGateway
16
+ private scimgateway: any
17
17
  private idleTimeout: number
18
18
  private graphUrl = 'https://graph.microsoft.com/beta' // beta instead of 'v1.0' gives all user attributes when no $select
19
19
 
20
- constructor(scimgateway: ScimGateway, optionalEntities?: Record<string, any>) {
21
- if (!(scimgateway instanceof ScimGateway)) throw new Error('HelperRest initialization error: argument scimgateway is not of type ScimGateway')
20
+ constructor(scimgateway: any, optionalEntities?: Record<string, any>) {
21
+ if (!scimgateway || !scimgateway.gwName) throw new Error('HelperRest initialization error: argument scimgateway is not of type ScimGateway')
22
22
  this.scimgateway = scimgateway
23
23
  this.idleTimeout = (scimgateway as any)?.config?.scimgateway.idleTimeout || 120
24
24
  this.idleTimeout = this.idleTimeout - 1
@@ -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 [Users] (
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 [Groups] (
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 [Groups]([GroupID])
37
+ // ON DELETE CASCADE,
38
+ // CONSTRAINT [FK_U2G_Users]
39
+ // FOREIGN KEY ([UserID])
40
+ // REFERENCES [Users]([UserID])
41
+ // ON DELETE CASCADE
42
+ // );
19
43
  //
20
44
  // Supported attributes:
21
45
  //
@@ -42,7 +66,7 @@ import { Connection, Request } from 'tedious'
42
66
  const ScimGateway: typeof import('scimgateway').ScimGateway = await (async () => {
43
67
  try {
44
68
  return (await import('scimgateway')).ScimGateway
45
- } catch (err) {
69
+ } catch (err: any) {
46
70
  const source = './scimgateway.ts'
47
71
  return (await import(source)).ScimGateway
48
72
  }
@@ -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,37 @@ 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) => {
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 => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
123
+ for (const user of users) {
124
+ const scimUser = {
125
+ id: user.UserID.value ? user.UserID.value : undefined,
126
+ userName: user.UserID.value ? user.UserID.value : undefined,
127
+ active: user.Enabled.value === 'true' || false,
128
+ name: {
129
+ givenName: user.FirstName.value ? user.FirstName.value : undefined,
130
+ middleName: user.MiddleName.value ? user.MiddleName.value : undefined,
131
+ familyName: user.LastName.value ? user.LastName.value : undefined,
132
+ },
133
+ phoneNumbers: user.MobilePhone.value ? [{ type: 'work', value: user.MobilePhone.value }] : undefined,
134
+ emails: user.Email.value ? [{ type: 'work', value: user.Email.value }] : undefined,
114
135
  }
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
- }
136
+ ret.Resources.push(scimUser)
137
+ }
121
138
 
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
139
+ resolve(ret) // all explored users
143
140
  }) // Promise
144
141
  } catch (err: any) {
145
142
  throw new Error(`${action} error: ${err.message}`)
@@ -154,7 +151,7 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
154
151
  scimgateway.logDebug(baseEntity, `handling "${action}" userObj=${JSON.stringify(userObj)}`)
155
152
 
156
153
  try {
157
- return await new Promise((resolve, reject) => {
154
+ return await new Promise(async (resolve) => {
158
155
  if (!userObj.name) userObj.name = {}
159
156
  if (!userObj.emails) userObj.emails = { work: {} }
160
157
  if (!userObj.phoneNumbers) userObj.phoneNumbers = { work: {} }
@@ -170,37 +167,12 @@ scimgateway.createUser = async (baseEntity, userObj, ctx) => {
170
167
  Email: (userObj.emails.work.value) ? `'${userObj.emails.work.value}'` : null,
171
168
  }
172
169
 
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)
170
+ const sqlQuery = `insert into [Users] (UserID, Enabled, Password, FirstName, MiddleName, LastName, Email, MobilePhone)
190
171
  values (${insert.UserID}, ${insert.Enabled}, ${insert.Password}, ${insert.FirstName}, ${insert.MiddleName}, ${insert.LastName}, ${insert.Email}, ${insert.MobilePhone})`
191
172
 
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
173
+ await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
174
+
175
+ resolve(null)
204
176
  }) // Promise
205
177
  } catch (err: any) {
206
178
  throw new Error(`${action} error: ${err.message}`)
@@ -215,36 +187,11 @@ scimgateway.deleteUser = async (baseEntity, id, ctx) => {
215
187
  scimgateway.logDebug(baseEntity, `handling "${action}" id=${id}`)
216
188
 
217
189
  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)
190
+ return await new Promise(async (resolve) => {
191
+ const sqlQuery = `delete from [Users] where UserID = '${id}'`
192
+ await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
229
193
 
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
194
+ resolve(null)
248
195
  }) // Promise
249
196
  } catch (err: any) {
250
197
  throw new Error(`${action} error: ${err.message}`)
@@ -259,7 +206,7 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
259
206
  scimgateway.logDebug(baseEntity, `handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
260
207
 
261
208
  try {
262
- return await new Promise((resolve, reject) => {
209
+ return await new Promise(async (resolve) => {
263
210
  if (!attrObj.name) attrObj.name = {}
264
211
  if (!attrObj.emails) attrObj.emails = { work: {} }
265
212
  if (!attrObj.phoneNumbers) attrObj.phoneNumbers = { work: {} }
@@ -294,35 +241,10 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
294
241
 
295
242
  sql = sql.substr(0, sql.length - 1) // remove trailing ","
296
243
 
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)
244
+ const sqlQuery = `update [Users] set ${sql} where UserID like '${id}'`
245
+ await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
307
246
 
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
247
+ resolve(null)
326
248
  }) // Promise
327
249
  } catch (err: any) {
328
250
  throw new Error(`${action} error: ${err.message}`)
@@ -332,55 +254,164 @@ scimgateway.modifyUser = async (baseEntity, id, attrObj, ctx) => {
332
254
  // =================================================
333
255
  // getGroups
334
256
  // =================================================
335
- scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
257
+ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
336
258
  const action = 'getGroups'
337
259
  scimgateway.logDebug(baseEntity, `handling "${action}" getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
338
260
 
261
+ let sqlQuery
262
+
339
263
  // mandatory if-else logic - start
340
264
  if (getObj.operator) {
341
265
  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
266
+ // mandatory - unique filtering - single unique user to be returned - correspond to getGroup() in versions < 4.x.x
267
+ sqlQuery = `select * from [Groups] where GroupID = '${getObj.value}'`
343
268
  } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
344
269
  // 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>}] }]
270
+ sqlQuery = `select * from [Groups] join [Users2Group] on Groups.GroupID = Users2Group.GroupID where Users2Group.UserID = '${getObj.value}'`
346
271
  } else {
347
272
  // optional - simpel filtering
273
+ throw new Error(`${action} error: not supporting simple filtering: ${getObj.rawFilter}`)
348
274
  }
349
275
  } else if (getObj.rawFilter) {
350
276
  // optional - advanced filtering having and/or/not - use getObj.rawFilter
277
+ throw new Error(`${action} not error: supporting advanced filtering: ${getObj.rawFilter}`)
351
278
  } else {
352
279
  // mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all groups to be returned - correspond to exploreGroups() in versions < 4.x.x
280
+ sqlQuery = 'select * from [Groups]'
353
281
  }
354
282
  // mandatory if-else logic - end
283
+ if (!sqlQuery) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
355
284
 
356
- return { Resources: [] } // groups not supported - returning empty Resources
285
+ try {
286
+ return await new Promise(async (resolve) => {
287
+ const ret: any = { // itemsPerPage will be set by scimgateway
288
+ Resources: [],
289
+ totalResults: null,
290
+ }
291
+
292
+ const groups: any[] = await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
293
+
294
+ for (const group of groups) {
295
+ const scimGroup: Record<string, any> = {
296
+ id: group.GroupID.value ? group.GroupID.value : undefined,
297
+ displayName: group.GroupID.value ? group.GroupID.value : undefined,
298
+ active: group.Enabled.value === 'true' || false,
299
+ members: [],
300
+ }
301
+
302
+ const sqlQuery = `select UserID from [Users2Group] where GroupID = '${scimGroup.id}'`
303
+ const members = await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
304
+ for (const member of members) {
305
+ const scimMember = {
306
+ value: member.UserID.value,
307
+ display: member.UserID.value,
308
+ }
309
+ scimGroup.members.push(scimMember)
310
+ }
311
+
312
+ ret.Resources.push(scimGroup)
313
+ }
314
+
315
+ resolve(ret)
316
+ }) // Promise
317
+ } catch (err: any) {
318
+ throw new Error(`${action} error: ${err.message}`)
319
+ }
357
320
  }
358
321
 
359
322
  // =================================================
360
323
  // createGroup
361
324
  // =================================================
362
- scimgateway.createGroup = async (baseEntity, groupObj) => {
325
+ scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
363
326
  const action = 'createGroup'
364
327
  scimgateway.logDebug(baseEntity, `handling "${action}" groupObj=${JSON.stringify(groupObj)}`)
365
- throw new Error(`${action} error: ${action} is not supported`)
328
+
329
+ try {
330
+ return await new Promise(async (resolve) => {
331
+ const insert = {
332
+ GroupID: `'${groupObj.displayName}'`,
333
+ Enabled: (groupObj.active) ? `'${groupObj.active}'` : '\'false\'',
334
+ }
335
+
336
+ const sqlQuery = `insert into [Groups] (GroupID, Enabled) values (${insert.GroupID}, ${insert.Enabled})`
337
+ await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
338
+
339
+ if (Array.isArray(groupObj.members) && groupObj.members) {
340
+ for (const member of groupObj.members) {
341
+ const sqlQuery = `insert into [Users2Group] (UserID, GroupID) values ('${member.value}', ${insert.GroupID})`
342
+ await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
343
+ }
344
+ }
345
+
346
+ resolve(null)
347
+ }) // Promise
348
+ } catch (err: any) {
349
+ throw new Error(`${action} error: ${err.message}`)
350
+ }
366
351
  }
367
352
 
368
353
  // =================================================
369
354
  // deleteGroup
370
355
  // =================================================
371
- scimgateway.deleteGroup = async (baseEntity, id) => {
356
+ scimgateway.deleteGroup = async (baseEntity, id, ctx) => {
372
357
  const action = 'deleteGroup'
373
358
  scimgateway.logDebug(baseEntity, `handling "${action}" id=${id}`)
374
- throw new Error(`${action} error: ${action} is not supported`)
359
+
360
+ try {
361
+ return await new Promise(async (resolve) => {
362
+ const sqlQuery = `delete from [Groups] where GroupID = '${id}'`
363
+ await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
364
+
365
+ resolve(null)
366
+ }) // Promise
367
+ } catch (err: any) {
368
+ throw new Error(`${action} error: ${err.message}`)
369
+ }
375
370
  }
376
371
 
377
372
  // =================================================
378
373
  // modifyGroup
379
374
  // =================================================
380
- scimgateway.modifyGroup = async (baseEntity, id, attrObj) => {
375
+ scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
381
376
  const action = 'modifyGroup'
382
377
  scimgateway.logDebug(baseEntity, `handling "${action}" id=${id} attrObj=${JSON.stringify(attrObj)}`)
383
- throw new Error(`${action} error: ${action} is not supported`)
378
+
379
+ let sql = ''
380
+
381
+ if (attrObj.active !== undefined) sql += `Enabled='${attrObj.active}',`
382
+ sql = sql.substr(0, sql.length - 1) // remove trailing ","
383
+
384
+ const queries = []
385
+ if (sql) {
386
+ queries.push(`update [Groups] set ${sql} where GroupID like '${id}'`)
387
+ }
388
+
389
+ // This BLINDLY inserts all user/groups and gracefully breaks on PK violation
390
+ // for each existing membership
391
+ if (Array.isArray(attrObj.members) && attrObj.members) {
392
+ for (const member of attrObj.members) {
393
+ if (member.operation == 'delete') {
394
+ queries.push(`delete from [Users2Group] where GroupID='${id}' and UserID='${member.value}'`)
395
+ } else {
396
+ queries.push(`insert into [Users2Group] (UserID, GroupID) values ('${member.value}','${id}')`)
397
+ }
398
+ }
399
+ }
400
+
401
+ const sqlQuery = queries.join(';')
402
+
403
+ try {
404
+ return await new Promise(async (resolve) => {
405
+ if (sqlQuery) {
406
+ scimgateway.logDebug(baseEntity, `sqlQuery: ${sqlQuery}`)
407
+ await query(sqlQuery, ctx).catch(err => scimgateway.logWarn(baseEntity, `${action} warning: ${err.message}`))
408
+ }
409
+
410
+ resolve(null)
411
+ }) // Promise
412
+ } catch (err: any) {
413
+ throw new Error(`${action} error: ${err.message}`)
414
+ }
384
415
  }
385
416
 
386
417
  // =================================================
@@ -399,6 +430,42 @@ const getCtxAuth = (ctx: undefined | Record<string, any>) => {
399
430
  else return [undefined, authToken] // bearer auth
400
431
  }
401
432
 
433
+ const connectionCfg = (ctx: undefined | Record<string, any>) => {
434
+ const connectionCfg = scimgateway.copyObj(config.connection)
435
+ if (ctx?.request?.header?.authorization) { // Auth PassThrough (don't use configuration password)
436
+ if (!connectionCfg.authentication) connectionCfg.authentication = {}
437
+ if (!connectionCfg.authentication.type) connectionCfg.authentication.type = 'default'
438
+ if (!connectionCfg.authentication.options) connectionCfg.authentication.options = {}
439
+ const [username, password] = getCtxAuth(ctx)
440
+ connectionCfg.authentication.options.password = password
441
+ if (username) connectionCfg.authentication.options.userName = username
442
+ }
443
+ return connectionCfg
444
+ }
445
+
446
+ const query: (sql: string, ctx: any) => Promise<any> = (sql, ctx) => new Promise((resolve, reject) => {
447
+ const connection = new Connection(connectionCfg(ctx))
448
+
449
+ connection.connect((err) => {
450
+ if (err) {
451
+ const e = new Error(`MSSQL client connect error: ${err.message}`)
452
+ reject(e)
453
+ } else {
454
+ const request = new Request(sql, (err, rowCount, rows) => {
455
+ if (err) {
456
+ connection.close()
457
+ const e = new Error(`MSSQL client request: ${sql} Error: ${err.message}`)
458
+ reject(e)
459
+ } else {
460
+ connection.close()
461
+ resolve(rows)
462
+ }
463
+ })
464
+ connection.execSql(request)
465
+ }
466
+ })
467
+ })
468
+
402
469
  //
403
470
  // Cleanup on exit
404
471
  //
@@ -2367,13 +2367,25 @@ export class ScimGateway {
2367
2367
  if (!ctx.response.body) ctx.response.body = 'NOT_FOUND'
2368
2368
  break
2369
2369
  }
2370
- const response = new Response(ctx.response.body, { status: ctx.response.status, headers: ctx.response.headers })
2371
- if (ctx.response.body) {
2370
+ const body = ctx.response.body
2371
+ if (body) {
2372
2372
  try {
2373
- JSON.parse(ctx.response.body)
2374
- response.headers.set('content-type', 'application/scim+json; charset=utf-8')
2373
+ JSON.parse(body)
2374
+ ctx.response.headers.set('content-type', 'application/scim+json; charset=utf-8')
2375
2375
  } catch (err) { void 0 }
2376
2376
  }
2377
+ let response: Response
2378
+ if (typeof Bun !== 'undefined') {
2379
+ const stream = new ReadableStream({ // ensure Bun compatibility with Azure Reverse Proxy for large and long running response - header set by Bun: Transfer-Encoding: 'chunked'
2380
+ start(controller) {
2381
+ controller.enqueue(body)
2382
+ controller.close()
2383
+ },
2384
+ })
2385
+ response = new Response(body ? stream : undefined, { status: ctx.response.status, headers: ctx.response.headers })
2386
+ } else {
2387
+ response = new Response(body ? body : undefined, { status: ctx.response.status, headers: ctx.response.headers })
2388
+ }
2377
2389
  logResult(ctx)
2378
2390
  return response
2379
2391
  }
@@ -2688,21 +2700,28 @@ export class ScimGateway {
2688
2700
  * logDebug logs debug message
2689
2701
  **/
2690
2702
  logDebug(baseEntity: string | undefined, msg: string) {
2691
- this.logger.debug(`${this.pluginName}[${baseEntity}]} ${msg}`)
2703
+ this.logger.debug(`${this.pluginName}[${baseEntity}] ${msg}`)
2692
2704
  }
2693
2705
 
2694
2706
  /**
2695
2707
  * logInfo logs info message
2696
2708
  **/
2697
2709
  logInfo(baseEntity: string | undefined, msg: string) {
2698
- this.logger.info(`${this.pluginName}[${baseEntity}]} ${msg}`)
2710
+ this.logger.info(`${this.pluginName}[${baseEntity}] ${msg}`)
2711
+ }
2712
+
2713
+ /**
2714
+ * logWarn logs warning message
2715
+ **/
2716
+ logWarn(baseEntity: string | undefined, msg: string) {
2717
+ this.logger.warn(`${this.pluginName}[${baseEntity}] ${msg}`)
2699
2718
  }
2700
2719
 
2701
2720
  /**
2702
2721
  * logError logs error message
2703
2722
  **/
2704
2723
  logError(baseEntity: string | undefined, msg: string) {
2705
- this.logger.error(`${this.pluginName}[${baseEntity}]} ${msg}`)
2724
+ this.logger.error(`${this.pluginName}[${baseEntity}] ${msg}`)
2706
2725
  }
2707
2726
 
2708
2727
  /**
@@ -2919,15 +2938,28 @@ export class ScimGateway {
2919
2938
  if (!this.helperRest) this.helperRest = new HelperRest(this, { entity: { undefined: { connection: this.config.scimgateway.email } } })
2920
2939
  if (this.config.scimgateway.email.auth?.options?.tenantIdGUID) {
2921
2940
  // Graph API
2941
+ let content: string
2942
+ if (isHtml) { // ExO workaround for national special caracters in html content - require singleValueExtendedProperties being used
2943
+ var enc = new TextEncoder() // utf-8
2944
+ const buf = enc.encode(msgObj.content)
2945
+ content = new TextDecoder('windows-1252').decode(buf)
2946
+ } else content = msgObj.content
2947
+
2922
2948
  const emailMessage: Record<string, any> = {
2923
2949
  message: {
2924
2950
  subject: msgObj.subject ? msgObj.subject : 'SCIM Gateway message',
2925
2951
  body: {
2926
- content: msgObj.content,
2952
+ content,
2927
2953
  contentType: isHtml ? 'HTML' : 'Text',
2928
2954
  },
2929
2955
  toRecipients: [],
2930
2956
  ccRecipients: [],
2957
+ singleValueExtendedProperties: [ // force using ExO header: Content-Type: text/plain; charset="utf-8"
2958
+ {
2959
+ id: 'Integer 0x3fde', // Content-Type header - can be verifed by checking raw mail
2960
+ value: '65001', // text/plain; charset="utf-8"
2961
+ },
2962
+ ],
2931
2963
  },
2932
2964
  saveToSentItems: 'false',
2933
2965
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "5.0.6",
3
+ "version": "5.0.8",
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)",