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 +122 -23
- package/config/docker/dbinit/init.sql +43 -0
- package/config/docker/docker-compose-mssql.yml +58 -0
- package/config/plugin-api.json +2 -2
- package/config/plugin-entra-id.json +2 -2
- package/config/plugin-ldap.json +2 -2
- package/config/plugin-loki.json +2 -2
- package/config/plugin-mongodb.json +2 -2
- package/config/plugin-mssql.json +2 -2
- package/config/plugin-saphana.json +2 -2
- package/config/plugin-scim.json +2 -2
- package/config/plugin-soap.json +2 -2
- package/lib/helper-rest.ts +13 -8
- package/lib/plugin-mssql.ts +222 -155
- package/lib/scimgateway.ts +40 -8
- package/package.json +1 -1
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
|
-
"
|
|
262
|
-
"
|
|
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. **`
|
|
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.
|
|
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
|
-
-
|
|
1106
|
-
-
|
|
1107
|
-
|
|
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
|
|
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
|
|
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, **
|
|
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
|
|
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
|
|
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
|
|
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
|
+
'
|
package/config/plugin-api.json
CHANGED
package/config/plugin-ldap.json
CHANGED
package/config/plugin-loki.json
CHANGED
package/config/plugin-mssql.json
CHANGED
package/config/plugin-scim.json
CHANGED
package/config/plugin-soap.json
CHANGED
package/lib/helper-rest.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
21
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
package/lib/plugin-mssql.ts
CHANGED
|
@@ -6,16 +6,40 @@
|
|
|
6
6
|
// Purpose: SQL user-provisioning
|
|
7
7
|
//
|
|
8
8
|
// Prereq:
|
|
9
|
-
// TABLE [
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
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 [
|
|
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 [
|
|
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
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
219
|
-
const
|
|
220
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
298
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
//
|
package/lib/scimgateway.ts
CHANGED
|
@@ -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
|
|
2371
|
-
if (
|
|
2370
|
+
const body = ctx.response.body
|
|
2371
|
+
if (body) {
|
|
2372
2372
|
try {
|
|
2373
|
-
JSON.parse(
|
|
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}]
|
|
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}]
|
|
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}]
|
|
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
|
|
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