@checkstack/auth-ldap-backend 0.0.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/package.json +1 -1
- package/src/helpers.ts +16 -0
- package/src/index.test.ts +109 -3
- package/src/index.ts +180 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @checkstack/auth-ldap-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d94121b: Add group-to-role mapping for SAML and LDAP authentication
|
|
8
|
+
|
|
9
|
+
**Features:**
|
|
10
|
+
|
|
11
|
+
- SAML and LDAP users can now be automatically assigned Checkstack roles based on their directory group memberships
|
|
12
|
+
- Configure group mappings in the authentication strategy settings with dynamic role dropdowns
|
|
13
|
+
- Managed role sync: roles configured in mappings are fully synchronized (added when user gains group, removed when user leaves group)
|
|
14
|
+
- Unmanaged roles (manually assigned, not in any mapping) are preserved during sync
|
|
15
|
+
- Optional default role for all users from a directory
|
|
16
|
+
|
|
17
|
+
**Bug Fix:**
|
|
18
|
+
|
|
19
|
+
- Fixed `x-options-resolver` not working for fields inside arrays with `.default([])` in DynamicForm schemas
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [d94121b]
|
|
24
|
+
- @checkstack/backend-api@0.3.3
|
|
25
|
+
- @checkstack/auth-backend@0.4.0
|
|
26
|
+
- @checkstack/auth-common@0.5.0
|
|
27
|
+
|
|
28
|
+
## 0.0.8
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- Updated dependencies [993d81a]
|
|
33
|
+
- Updated dependencies [df6ac7b]
|
|
34
|
+
- @checkstack/auth-backend@0.3.0
|
|
35
|
+
- @checkstack/auth-common@0.4.0
|
|
36
|
+
|
|
3
37
|
## 0.0.7
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/package.json
CHANGED
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper to extract groups from LDAP entry (multi-valued attribute)
|
|
3
|
+
* Returns all group DNs as an array of strings
|
|
4
|
+
*/
|
|
5
|
+
export const extractGroups = ({
|
|
6
|
+
ldapEntry,
|
|
7
|
+
memberOfAttribute,
|
|
8
|
+
}: {
|
|
9
|
+
ldapEntry: Record<string, unknown>;
|
|
10
|
+
memberOfAttribute: string;
|
|
11
|
+
}): string[] => {
|
|
12
|
+
const value = ldapEntry[memberOfAttribute];
|
|
13
|
+
if (typeof value === "string") return [value];
|
|
14
|
+
if (Array.isArray(value)) return value.map(String);
|
|
15
|
+
return [];
|
|
16
|
+
};
|
package/src/index.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, beforeEach, mock } from "bun:test";
|
|
2
2
|
import type { Client as LdapClient } from "ldapts";
|
|
3
|
+
import { extractGroups } from "./helpers";
|
|
3
4
|
|
|
4
5
|
describe("LDAP Authentication Strategy", () => {
|
|
5
6
|
// Mock LDAP client
|
|
@@ -24,7 +25,7 @@ describe("LDAP Authentication Strategy", () => {
|
|
|
24
25
|
sn: "User",
|
|
25
26
|
},
|
|
26
27
|
],
|
|
27
|
-
})
|
|
28
|
+
}),
|
|
28
29
|
),
|
|
29
30
|
unbind: mock(() => Promise.resolve()),
|
|
30
31
|
};
|
|
@@ -38,7 +39,7 @@ describe("LDAP Authentication Strategy", () => {
|
|
|
38
39
|
await mockLdapClient.bind("cn=admin,dc=example,dc=com", "adminPassword");
|
|
39
40
|
expect(mockLdapClient.bind).toHaveBeenCalledWith(
|
|
40
41
|
"cn=admin,dc=example,dc=com",
|
|
41
|
-
"adminPassword"
|
|
42
|
+
"adminPassword",
|
|
42
43
|
);
|
|
43
44
|
});
|
|
44
45
|
|
|
@@ -47,7 +48,7 @@ describe("LDAP Authentication Strategy", () => {
|
|
|
47
48
|
mockLdapClient.bind.mockRejectedValue(new Error("Invalid credentials"));
|
|
48
49
|
|
|
49
50
|
await expect(
|
|
50
|
-
mockLdapClient.bind("uid=testuser,ou=users,dc=example,dc=com", "wrong")
|
|
51
|
+
mockLdapClient.bind("uid=testuser,ou=users,dc=example,dc=com", "wrong"),
|
|
51
52
|
).rejects.toThrow("Invalid credentials");
|
|
52
53
|
});
|
|
53
54
|
|
|
@@ -270,3 +271,108 @@ describe("LDAP Authentication Strategy", () => {
|
|
|
270
271
|
});
|
|
271
272
|
});
|
|
272
273
|
});
|
|
274
|
+
|
|
275
|
+
describe("extractGroups helper for LDAP", () => {
|
|
276
|
+
it("should extract single group as array", () => {
|
|
277
|
+
const ldapEntry = {
|
|
278
|
+
memberOf: "CN=Developers,OU=Groups,DC=example,DC=com",
|
|
279
|
+
};
|
|
280
|
+
const result = extractGroups({
|
|
281
|
+
ldapEntry,
|
|
282
|
+
memberOfAttribute: "memberOf",
|
|
283
|
+
});
|
|
284
|
+
expect(result).toEqual(["CN=Developers,OU=Groups,DC=example,DC=com"]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should extract multiple groups from array", () => {
|
|
288
|
+
const ldapEntry = {
|
|
289
|
+
memberOf: [
|
|
290
|
+
"CN=Developers,OU=Groups,DC=example,DC=com",
|
|
291
|
+
"CN=All-Users,OU=Groups,DC=example,DC=com",
|
|
292
|
+
"CN=Admins,OU=Groups,DC=example,DC=com",
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
const result = extractGroups({
|
|
296
|
+
ldapEntry,
|
|
297
|
+
memberOfAttribute: "memberOf",
|
|
298
|
+
});
|
|
299
|
+
expect(result).toEqual([
|
|
300
|
+
"CN=Developers,OU=Groups,DC=example,DC=com",
|
|
301
|
+
"CN=All-Users,OU=Groups,DC=example,DC=com",
|
|
302
|
+
"CN=Admins,OU=Groups,DC=example,DC=com",
|
|
303
|
+
]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should return empty array for missing memberOf attribute", () => {
|
|
307
|
+
const ldapEntry = {
|
|
308
|
+
uid: "testuser",
|
|
309
|
+
mail: "test@example.com",
|
|
310
|
+
};
|
|
311
|
+
const result = extractGroups({
|
|
312
|
+
ldapEntry,
|
|
313
|
+
memberOfAttribute: "memberOf",
|
|
314
|
+
});
|
|
315
|
+
expect(result).toEqual([]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should return empty array for empty memberOf array", () => {
|
|
319
|
+
const ldapEntry = {
|
|
320
|
+
memberOf: [],
|
|
321
|
+
};
|
|
322
|
+
const result = extractGroups({
|
|
323
|
+
ldapEntry,
|
|
324
|
+
memberOfAttribute: "memberOf",
|
|
325
|
+
});
|
|
326
|
+
expect(result).toEqual([]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should handle custom memberOf attribute names", () => {
|
|
330
|
+
const ldapEntry = {
|
|
331
|
+
isMemberOf: ["group1", "group2"],
|
|
332
|
+
};
|
|
333
|
+
const result = extractGroups({
|
|
334
|
+
ldapEntry,
|
|
335
|
+
memberOfAttribute: "isMemberOf",
|
|
336
|
+
});
|
|
337
|
+
expect(result).toEqual(["group1", "group2"]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("should handle undefined/null values gracefully", () => {
|
|
341
|
+
const ldapEntry = {
|
|
342
|
+
memberOf: undefined,
|
|
343
|
+
};
|
|
344
|
+
const result = extractGroups({
|
|
345
|
+
ldapEntry,
|
|
346
|
+
memberOfAttribute: "memberOf",
|
|
347
|
+
});
|
|
348
|
+
expect(result).toEqual([]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("should convert non-string group values to strings", () => {
|
|
352
|
+
const ldapEntry = {
|
|
353
|
+
memberOf: [123, "CN=Group,DC=test", true],
|
|
354
|
+
};
|
|
355
|
+
const result = extractGroups({
|
|
356
|
+
ldapEntry,
|
|
357
|
+
memberOfAttribute: "memberOf",
|
|
358
|
+
});
|
|
359
|
+
expect(result).toEqual(["123", "CN=Group,DC=test", "true"]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should handle Active Directory group DNs", () => {
|
|
363
|
+
const ldapEntry = {
|
|
364
|
+
memberOf: [
|
|
365
|
+
"CN=Domain Users,CN=Users,DC=corp,DC=example,DC=com",
|
|
366
|
+
"CN=Engineering,OU=Security Groups,DC=corp,DC=example,DC=com",
|
|
367
|
+
],
|
|
368
|
+
};
|
|
369
|
+
const result = extractGroups({
|
|
370
|
+
ldapEntry,
|
|
371
|
+
memberOfAttribute: "memberOf",
|
|
372
|
+
});
|
|
373
|
+
expect(result).toEqual([
|
|
374
|
+
"CN=Domain Users,CN=Users,DC=corp,DC=example,DC=com",
|
|
375
|
+
"CN=Engineering,OU=Security Groups,DC=corp,DC=example,DC=com",
|
|
376
|
+
]);
|
|
377
|
+
});
|
|
378
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { AuthApi } from "@checkstack/auth-common";
|
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import { Client as LdapClient } from "ldapts";
|
|
17
17
|
import { hashPassword } from "better-auth/crypto";
|
|
18
|
+
import { extractGroups } from "./helpers";
|
|
18
19
|
|
|
19
20
|
// LDAP Configuration Schema V1
|
|
20
21
|
const _ldapConfigV1 = z.object({
|
|
@@ -28,7 +29,7 @@ const _ldapConfigV1 = z.object({
|
|
|
28
29
|
bindDN: configString({})
|
|
29
30
|
.optional()
|
|
30
31
|
.describe(
|
|
31
|
-
"Service account DN for searching (e.g., cn=admin,dc=example,dc=com)"
|
|
32
|
+
"Service account DN for searching (e.g., cn=admin,dc=example,dc=com)",
|
|
32
33
|
),
|
|
33
34
|
bindPassword: configString({ "x-secret": true })
|
|
34
35
|
.describe("Service account password")
|
|
@@ -86,8 +87,8 @@ const _ldapConfigV1 = z.object({
|
|
|
86
87
|
.describe("Update user attributes on each login"),
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
// LDAP Configuration Schema
|
|
90
|
-
const
|
|
90
|
+
// LDAP Configuration Schema V2 (kept for migration type reference)
|
|
91
|
+
const _ldapConfigV2 = z.object({
|
|
91
92
|
url: configString({})
|
|
92
93
|
.url()
|
|
93
94
|
.default("ldaps://ldap.example.com:636")
|
|
@@ -95,7 +96,7 @@ const ldapConfigV2 = z.object({
|
|
|
95
96
|
bindDN: configString({})
|
|
96
97
|
.optional()
|
|
97
98
|
.describe(
|
|
98
|
-
"Service account DN for searching (e.g., cn=admin,dc=example,dc=com)"
|
|
99
|
+
"Service account DN for searching (e.g., cn=admin,dc=example,dc=com)",
|
|
99
100
|
),
|
|
100
101
|
bindPassword: configString({ "x-secret": true })
|
|
101
102
|
.describe("Service account password")
|
|
@@ -153,7 +154,108 @@ const ldapConfigV2 = z.object({
|
|
|
153
154
|
.describe("Update user attributes on each login"),
|
|
154
155
|
});
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
// LDAP Configuration Schema V3 - Adds group-to-role mapping
|
|
158
|
+
const ldapConfigV3 = z.object({
|
|
159
|
+
url: configString({})
|
|
160
|
+
.url()
|
|
161
|
+
.default("ldaps://ldap.example.com:636")
|
|
162
|
+
.describe("LDAP server URL (e.g., ldaps://ldap.example.com:636)"),
|
|
163
|
+
bindDN: configString({})
|
|
164
|
+
.optional()
|
|
165
|
+
.describe(
|
|
166
|
+
"Service account DN for searching (e.g., cn=admin,dc=example,dc=com)",
|
|
167
|
+
),
|
|
168
|
+
bindPassword: configString({ "x-secret": true })
|
|
169
|
+
.describe("Service account password")
|
|
170
|
+
.optional(),
|
|
171
|
+
baseDN: configString({})
|
|
172
|
+
.default("ou=users,dc=example,dc=com")
|
|
173
|
+
.describe("Base DN for user searches (e.g., ou=users,dc=example,dc=com)"),
|
|
174
|
+
searchFilter: configString({})
|
|
175
|
+
.default("(uid={0})")
|
|
176
|
+
.describe("LDAP search filter, {0} will be replaced with username"),
|
|
177
|
+
usernameAttribute: configString({})
|
|
178
|
+
.default("uid")
|
|
179
|
+
.describe("LDAP attribute to match against login username"),
|
|
180
|
+
attributeMapping: z
|
|
181
|
+
.object({
|
|
182
|
+
email: configString({})
|
|
183
|
+
.default("mail")
|
|
184
|
+
.describe("LDAP attribute for email address"),
|
|
185
|
+
name: configString({})
|
|
186
|
+
.default("displayName")
|
|
187
|
+
.describe("LDAP attribute for display name"),
|
|
188
|
+
firstName: configString({})
|
|
189
|
+
.default("givenName")
|
|
190
|
+
.describe("LDAP attribute for first name")
|
|
191
|
+
.optional(),
|
|
192
|
+
lastName: configString({})
|
|
193
|
+
.default("sn")
|
|
194
|
+
.describe("LDAP attribute for last name")
|
|
195
|
+
.optional(),
|
|
196
|
+
})
|
|
197
|
+
.default({
|
|
198
|
+
email: "mail",
|
|
199
|
+
name: "displayName",
|
|
200
|
+
})
|
|
201
|
+
.describe("Map LDAP attributes to user fields"),
|
|
202
|
+
// Group to Role Mapping
|
|
203
|
+
groupMapping: z
|
|
204
|
+
.object({
|
|
205
|
+
enabled: configBoolean({})
|
|
206
|
+
.default(false)
|
|
207
|
+
.describe("Enable group-to-role mapping"),
|
|
208
|
+
memberOfAttribute: configString({})
|
|
209
|
+
.default("memberOf")
|
|
210
|
+
.describe("LDAP attribute containing group memberships"),
|
|
211
|
+
mappings: z
|
|
212
|
+
.array(
|
|
213
|
+
z.object({
|
|
214
|
+
directoryGroup: configString({}).describe(
|
|
215
|
+
"Directory group DN (e.g., cn=developers,ou=groups,dc=example,dc=com)",
|
|
216
|
+
),
|
|
217
|
+
checkstackRole: configString({
|
|
218
|
+
"x-options-resolver": "roleOptions",
|
|
219
|
+
}).describe("Checkstack role ID to assign"),
|
|
220
|
+
}),
|
|
221
|
+
)
|
|
222
|
+
.default([])
|
|
223
|
+
.describe("Map directory groups to Checkstack roles"),
|
|
224
|
+
defaultRole: configString({
|
|
225
|
+
"x-options-resolver": "roleOptions",
|
|
226
|
+
})
|
|
227
|
+
.optional()
|
|
228
|
+
.describe("Default role assigned to all LDAP users (optional)"),
|
|
229
|
+
})
|
|
230
|
+
.default({
|
|
231
|
+
enabled: false,
|
|
232
|
+
memberOfAttribute: "memberOf",
|
|
233
|
+
mappings: [],
|
|
234
|
+
})
|
|
235
|
+
.describe("Map LDAP groups to Checkstack roles"),
|
|
236
|
+
tlsOptions: z
|
|
237
|
+
.object({
|
|
238
|
+
rejectUnauthorized: configBoolean({})
|
|
239
|
+
.default(true)
|
|
240
|
+
.describe("Reject unauthorized SSL certificates"),
|
|
241
|
+
ca: configString({ "x-secret": true })
|
|
242
|
+
.describe("Custom CA certificate (PEM format)")
|
|
243
|
+
.optional(),
|
|
244
|
+
})
|
|
245
|
+
.default({ rejectUnauthorized: true })
|
|
246
|
+
.describe("TLS/SSL configuration"),
|
|
247
|
+
timeout: configNumber({})
|
|
248
|
+
.default(5000)
|
|
249
|
+
.describe("Connection timeout in milliseconds"),
|
|
250
|
+
autoCreateUsers: configBoolean({})
|
|
251
|
+
.default(true)
|
|
252
|
+
.describe("Automatically create users on first login"),
|
|
253
|
+
autoUpdateUsers: configBoolean({})
|
|
254
|
+
.default(true)
|
|
255
|
+
.describe("Update user attributes on each login"),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
type LdapConfig = z.infer<typeof ldapConfigV3>;
|
|
157
259
|
|
|
158
260
|
// LDAP Strategy Definition
|
|
159
261
|
const ldapStrategy: AuthStrategy<LdapConfig> = {
|
|
@@ -161,8 +263,8 @@ const ldapStrategy: AuthStrategy<LdapConfig> = {
|
|
|
161
263
|
displayName: "LDAP",
|
|
162
264
|
description: "Authenticate using LDAP directory",
|
|
163
265
|
icon: "Network",
|
|
164
|
-
configVersion:
|
|
165
|
-
configSchema:
|
|
266
|
+
configVersion: 3,
|
|
267
|
+
configSchema: ldapConfigV3,
|
|
166
268
|
requiresManualRegistration: false,
|
|
167
269
|
adminInstructions: `
|
|
168
270
|
## LDAP Configuration
|
|
@@ -175,6 +277,13 @@ Configure LDAP authentication to allow users from your directory to sign in:
|
|
|
175
277
|
4. Configure the **search filter** to match usernames (e.g., \`(uid={0})\` or \`(sAMAccountName={0})\`)
|
|
176
278
|
5. Map **LDAP attributes** to user fields (email, name)
|
|
177
279
|
|
|
280
|
+
### Group to Role Mapping
|
|
281
|
+
Map LDAP groups to Checkstack roles for automatic role assignment:
|
|
282
|
+
1. Enable **Group to Role Mapping**
|
|
283
|
+
2. Set the **Member Of Attribute** (usually \`memberOf\`)
|
|
284
|
+
3. Add mappings from directory group DNs to Checkstack roles
|
|
285
|
+
4. Optionally set a **Default Role** for all LDAP users
|
|
286
|
+
|
|
178
287
|
> **Active Directory**: Use \`ldaps://\` with port 636, \`sAMAccountName\` for username, and \`userPrincipalName\` for email.
|
|
179
288
|
`.trim(),
|
|
180
289
|
migrations: [
|
|
@@ -188,6 +297,19 @@ Configure LDAP authentication to allow users from your directory to sign in:
|
|
|
188
297
|
return rest;
|
|
189
298
|
},
|
|
190
299
|
},
|
|
300
|
+
{
|
|
301
|
+
description: "Add group-to-role mapping configuration",
|
|
302
|
+
fromVersion: 2,
|
|
303
|
+
toVersion: 3,
|
|
304
|
+
migrate: (oldConfig: z.infer<typeof _ldapConfigV2>) => ({
|
|
305
|
+
...oldConfig,
|
|
306
|
+
groupMapping: {
|
|
307
|
+
enabled: false,
|
|
308
|
+
memberOfAttribute: "memberOf",
|
|
309
|
+
mappings: [],
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
191
313
|
],
|
|
192
314
|
};
|
|
193
315
|
|
|
@@ -215,7 +337,7 @@ export default createBackendPlugin({
|
|
|
215
337
|
// Helper function to authenticate against LDAP
|
|
216
338
|
const authenticateLdap = async (
|
|
217
339
|
username: string,
|
|
218
|
-
password: string
|
|
340
|
+
password: string,
|
|
219
341
|
): Promise<{
|
|
220
342
|
success: boolean;
|
|
221
343
|
userAttributes?: Record<string, unknown>;
|
|
@@ -223,7 +345,7 @@ export default createBackendPlugin({
|
|
|
223
345
|
}> => {
|
|
224
346
|
try {
|
|
225
347
|
// Load LDAP configuration
|
|
226
|
-
const ldapConfig = await config.get("ldap",
|
|
348
|
+
const ldapConfig = await config.get("ldap", ldapConfigV3, 3);
|
|
227
349
|
|
|
228
350
|
if (!ldapConfig) {
|
|
229
351
|
return {
|
|
@@ -257,7 +379,7 @@ export default createBackendPlugin({
|
|
|
257
379
|
// Step 2: Search for the user
|
|
258
380
|
const searchFilter = ldapConfig.searchFilter.replace(
|
|
259
381
|
"{0}",
|
|
260
|
-
username
|
|
382
|
+
username,
|
|
261
383
|
);
|
|
262
384
|
const searchResult = await client.search(ldapConfig.baseDN, {
|
|
263
385
|
filter: searchFilter,
|
|
@@ -276,7 +398,7 @@ export default createBackendPlugin({
|
|
|
276
398
|
|
|
277
399
|
if (searchResult.searchEntries.length > 1) {
|
|
278
400
|
logger.warn(
|
|
279
|
-
`Multiple LDAP entries found for username: ${username}
|
|
401
|
+
`Multiple LDAP entries found for username: ${username}`,
|
|
280
402
|
);
|
|
281
403
|
}
|
|
282
404
|
|
|
@@ -335,9 +457,9 @@ export default createBackendPlugin({
|
|
|
335
457
|
// Helper function to create or update user via RPC
|
|
336
458
|
const syncUser = async (
|
|
337
459
|
username: string,
|
|
338
|
-
ldapAttributes: Record<string, unknown
|
|
460
|
+
ldapAttributes: Record<string, unknown>,
|
|
339
461
|
): Promise<{ userId: string; email: string; name: string }> => {
|
|
340
|
-
const ldapConfig = await config.get("ldap",
|
|
462
|
+
const ldapConfig = await config.get("ldap", ldapConfigV3, 3);
|
|
341
463
|
if (!ldapConfig) {
|
|
342
464
|
throw new Error("LDAP configuration not found");
|
|
343
465
|
}
|
|
@@ -370,7 +492,46 @@ export default createBackendPlugin({
|
|
|
370
492
|
const existingUser = await authClient.findUserByEmail({ email });
|
|
371
493
|
if (!existingUser) {
|
|
372
494
|
throw new Error(
|
|
373
|
-
"User does not exist and auto-creation is disabled"
|
|
495
|
+
"User does not exist and auto-creation is disabled",
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Extract groups and map to roles if enabled
|
|
501
|
+
let syncRoles: string[] | undefined;
|
|
502
|
+
let managedRoleIds: string[] | undefined;
|
|
503
|
+
if (ldapConfig.groupMapping?.enabled) {
|
|
504
|
+
const groups = extractGroups({
|
|
505
|
+
ldapEntry: ldapAttributes,
|
|
506
|
+
memberOfAttribute: ldapConfig.groupMapping.memberOfAttribute,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Map groups to roles
|
|
510
|
+
const mappedRoles = ldapConfig.groupMapping.mappings
|
|
511
|
+
.filter((m) => groups.includes(m.directoryGroup))
|
|
512
|
+
.map((m) => m.checkstackRole);
|
|
513
|
+
|
|
514
|
+
// Add default role if configured
|
|
515
|
+
if (ldapConfig.groupMapping.defaultRole) {
|
|
516
|
+
mappedRoles.push(ldapConfig.groupMapping.defaultRole);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Deduplicate roles
|
|
520
|
+
syncRoles = [...new Set(mappedRoles)];
|
|
521
|
+
|
|
522
|
+
// Collect all managed role IDs (all roles in mappings + default)
|
|
523
|
+
// These are roles controlled by directory - will be removed if user leaves groups
|
|
524
|
+
const allManagedRoles = ldapConfig.groupMapping.mappings.map(
|
|
525
|
+
(m) => m.checkstackRole,
|
|
526
|
+
);
|
|
527
|
+
if (ldapConfig.groupMapping.defaultRole) {
|
|
528
|
+
allManagedRoles.push(ldapConfig.groupMapping.defaultRole);
|
|
529
|
+
}
|
|
530
|
+
managedRoleIds = [...new Set(allManagedRoles)];
|
|
531
|
+
|
|
532
|
+
if (syncRoles.length > 0) {
|
|
533
|
+
logger.debug(
|
|
534
|
+
`LDAP user ${email} will be assigned roles: ${syncRoles.join(", ")}`,
|
|
374
535
|
);
|
|
375
536
|
}
|
|
376
537
|
}
|
|
@@ -385,6 +546,8 @@ export default createBackendPlugin({
|
|
|
385
546
|
accountId: username,
|
|
386
547
|
password: hashedPassword,
|
|
387
548
|
autoUpdateUser: ldapConfig.autoUpdateUsers,
|
|
549
|
+
syncRoles,
|
|
550
|
+
managedRoleIds,
|
|
388
551
|
});
|
|
389
552
|
|
|
390
553
|
if (created) {
|
|
@@ -412,14 +575,14 @@ export default createBackendPlugin({
|
|
|
412
575
|
|
|
413
576
|
if (!authResult.success) {
|
|
414
577
|
return redirectToAuthError(
|
|
415
|
-
authResult.error || "Authentication failed"
|
|
578
|
+
authResult.error || "Authentication failed",
|
|
416
579
|
);
|
|
417
580
|
}
|
|
418
581
|
|
|
419
582
|
// Sync user to database
|
|
420
583
|
const { userId, email, name } = await syncUser(
|
|
421
584
|
username,
|
|
422
|
-
authResult.userAttributes
|
|
585
|
+
authResult.userAttributes!,
|
|
423
586
|
);
|
|
424
587
|
|
|
425
588
|
// Create session via RPC
|
|
@@ -452,7 +615,7 @@ export default createBackendPlugin({
|
|
|
452
615
|
7 * 24 * 60 * 60
|
|
453
616
|
}`,
|
|
454
617
|
},
|
|
455
|
-
}
|
|
618
|
+
},
|
|
456
619
|
);
|
|
457
620
|
} catch (error) {
|
|
458
621
|
logger.error("LDAP login error:", error);
|