ldap-authentication 4.0.4 → 4.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.
@@ -8,14 +8,13 @@ services:
8
8
  command: sleep infinity
9
9
 
10
10
  ldap:
11
- image: symas/openldap:2.6
11
+ build:
12
+ context: ..
13
+ dockerfile: docker/ldap/Dockerfile
12
14
  ports:
13
- - '1389:1389'
14
- - '1636:1636'
15
+ - '1389:389'
16
+ - '1636:636'
15
17
  environment:
16
- - LDAP_ROOT=dc=example,dc=com
17
- - LDAP_ADMIN_USERNAME=read-only-admin
18
+ - LDAP_DOMAIN=example.com
18
19
  - LDAP_ADMIN_PASSWORD=password
19
- - LDAP_USERS=gauss,einstein
20
- - LDAP_PASSWORDS=password,password
21
- - LDAP_GROUP=科学A部
20
+ - LDAP_TLS=true
@@ -0,0 +1,53 @@
1
+ name: 'Publish to npm'
2
+
3
+ env:
4
+ INGITHUB: true
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ name: 'Publish ${{ github.ref_name }}'
14
+
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+
18
+ - name: Install LDAP tools
19
+ run: sudo apt-get install -y ldap-utils
20
+
21
+ - name: Start containers
22
+ run: docker compose -f "docker-compose.yml" up -d
23
+
24
+ - name: Wait for LDAP server to become available
25
+ uses: nick-fields/retry@v2
26
+ with:
27
+ timeout_minutes: 5
28
+ max_attempts: 10
29
+ polling_interval_seconds: 3
30
+ command: |
31
+ ldapsearch -LLL -o 'ldif-wrap=no' \
32
+ -H ldap://localhost:1389 \
33
+ -D 'cn=read-only-admin,dc=example,dc=com' \
34
+ -w password \
35
+ -b 'cn=einstein,ou=users,dc=example,dc=com' DN
36
+
37
+ - name: Use Node.js 22.x
38
+ uses: actions/setup-node@v3
39
+ with:
40
+ node-version: 22.x
41
+ registry-url: 'https://registry.npmjs.org'
42
+
43
+ - run: npm ci
44
+ - run: npm run test
45
+
46
+ - name: Stop containers
47
+ if: always()
48
+ run: docker compose -f "docker-compose.yml" down
49
+
50
+ - name: Publish to npm
51
+ run: npm publish
52
+ env:
53
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -25,7 +25,11 @@ This library use `ldapts` as the underneath library. It has three modes of authe
25
25
  then does a search on the user and return the user's details.
26
26
 
27
27
  3. **Verify user exists**. If an `verifyUserExists : true` is provided, the library will login (ldap bind) with the admin user,
28
- then search for the user to be verified. If the user exists, user details will be returned (without verifying the user's password).
28
+ then search for the user to be verified. If the user exists, user details will be returned (without verifying the user's password).
29
+
30
+ In addition, the `fetchUsers()` function can be used to fetch all users under a search base, using the admin
31
+ account to search (without any username or password of the individual users). The search always uses
32
+ LDAP paged results, so the common server-side limit of 1000 entries per search does not apply.
29
33
 
30
34
  ## Features
31
35
 
@@ -102,8 +106,32 @@ let authenticated = await authenticate({
102
106
  })
103
107
  ```
104
108
 
109
+ #### Fetch all users (admin search, without user passwords)
110
+
111
+ ```javascript
112
+ const { fetchUsers } = require('ldap-authentication')
113
+
114
+ let users = await fetchUsers({
115
+ ldapOpts: { url: 'ldap://ldap.forumsys.com' },
116
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
117
+ adminPassword: 'password',
118
+ userSearchBase: 'dc=example,dc=com',
119
+ // userFilter: '(objectClass=person)', // default: (|(uid=*)(sAMAccountName=*))
120
+ // attributes: ['uid', 'sn', 'mail'], // omitted = all attributes
121
+ // pageSize: 500, // default: 1000
122
+ })
123
+ ```
124
+
105
125
  #### Complete example
106
126
 
127
+ The library works with both CommonJS and ES modules:
128
+
129
+ ```javascript
130
+ import { authenticate } from 'ldap-authentication'
131
+ // or
132
+ const { authenticate } = require('ldap-authentication')
133
+ ```
134
+
107
135
  ```javascript
108
136
  const { authenticate } = require('ldap-authentication')
109
137
 
@@ -204,9 +232,18 @@ auth()
204
232
  if this value is not set, then authenticate will return true right after user bind succeed. No user details
205
233
  from LDAP search will be performed and returned.
206
234
  Example: `uid`
235
+ - `usernameFilter`: Prioritized alternative to usernameAttribute, allows you to provide a filter where `{{username}}` will
236
+ be replaced with the username provided
237
+ Example: `(|(uid={{username}})(mail={{username}}))`
207
238
  - `username`: The username to authenticate with. It is used together with the name in `usernameAttribute`
208
239
  to construct an ldap filter as `({attribute}={username})`
209
240
  to find the user and get user details in LDAP. Example: `some user input`
241
+ - `userFilter`: (used by `fetchUsers()`) The ldap search filter to select the users to return.
242
+ By default it is `(|(uid=*)(sAMAccountName=*))`, which matches both POSIX (`uid`)
243
+ and Active Directory (`sAMAccountName`) users. Example: `'(objectClass=person)'`,
244
+ or `'(objectClass=*)'` to match everything
245
+ - `pageSize`: (used by `fetchUsers()`) The number of entries to fetch per page for the paged
246
+ search. Default: `1000`
210
247
  - `attributes`: A list of attributes of a user details to be returned from the LDAP server.
211
248
  If is set to `[]` or ommited, all details will be returned. Example: `['sn', 'cn']`
212
249
  - `starttls`: Boolean. Use `STARTTLS` or not. When `true`, the connection will be upgraded to TLS
@@ -223,6 +260,8 @@ The user object if `authenticate()` is success.
223
260
 
224
261
  In version 4, a new function is added: `authenticateResult()`. It has the same call signature as `authenticate()` but returns an object `AuthenticationResult` with more details.
225
262
 
263
+ `fetchUsers()` returns an array of user objects, one per matched LDAP entry (each with its `dn` and the returned attributes), or an empty array if no user matches the filter.
264
+
226
265
 
227
266
  ### AuthenticationResult Object
228
267
 
@@ -0,0 +1,47 @@
1
+ # Test directory seed data for ldap-authentication integration tests.
2
+ # Loaded by osixia/openldap at first start via
3
+ # /container/service/slapd/assets/config/bootstrap/ldif/custom
4
+ # (directory bind-mounted in docker-compose.yml).
5
+ # See https://github.com/osixia/container-openldap (v1, tag 1.5.0)
6
+
7
+ dn: ou=users,dc=example,dc=com
8
+ objectClass: organizationalUnit
9
+ ou: users
10
+
11
+ dn: ou=groups,dc=example,dc=com
12
+ objectClass: organizationalUnit
13
+ ou: groups
14
+
15
+ dn: cn=read-only-admin,dc=example,dc=com
16
+ objectClass: simpleSecurityObject
17
+ objectClass: organizationalRole
18
+ cn: read-only-admin
19
+ description: LDAP admin user for tests (password: password)
20
+ userPassword: {SSHA}u/l8yXCyiWhP3iXpt0UWMyM9ch5sZGFwLXRlc3QtMjAyNg==
21
+
22
+ dn: cn=gauss,ou=users,dc=example,dc=com
23
+ objectClass: inetOrgPerson
24
+ objectClass: posixAccount
25
+ cn: gauss
26
+ sn: Bar1
27
+ uid: gauss
28
+ uidNumber: 1000
29
+ gidNumber: 1000
30
+ homeDirectory: /home/gauss
31
+ userPassword: {SSHA}u/l8yXCyiWhP3iXpt0UWMyM9ch5sZGFwLXRlc3QtMjAyNg==
32
+
33
+ dn: cn=einstein,ou=users,dc=example,dc=com
34
+ objectClass: inetOrgPerson
35
+ objectClass: posixAccount
36
+ cn: einstein
37
+ sn: Bar2
38
+ uid: einstein
39
+ uidNumber: 1001
40
+ gidNumber: 1001
41
+ homeDirectory: /home/einstein
42
+ userPassword: {SSHA}u/l8yXCyiWhP3iXpt0UWMyM9ch5sZGFwLXRlc3QtMjAyNg==
43
+
44
+ dn: cn=科学A部,ou=groups,dc=example,dc=com
45
+ objectClass: groupOfNames
46
+ cn: 科学A部
47
+ member: cn=gauss,ou=users,dc=example,dc=com
@@ -0,0 +1,21 @@
1
+ # ACLs for the test LDAP directory, applied on top of the osixia/openldap
2
+ # defaults (replaces the data database access rules at first start).
3
+ #
4
+ # The userPassword rule must come before the generic "to *" rule: OpenLDAP
5
+ # evaluates the first matching "to" clause for an attribute.
6
+ #
7
+ # - EXTERNAL (slapd local socket): full manage (used by the image bootstrap)
8
+ # - cn=admin,dc=example,dc=com: full manage
9
+ # - cn=read-only-admin,dc=example,dc=com: full manage (tests use it as the
10
+ # admin, including the entry modify in binary.spec.js)
11
+ # - normal users: read own entry and read group entries (group lookup tests)
12
+ # - anonymous: no access (simple bind does not consult other ACLs)
13
+
14
+ dn: olcDatabase={1}mdb,cn=config
15
+ changetype: modify
16
+ delete: olcAccess
17
+ -
18
+ add: olcAccess
19
+ olcAccess: to attrs=userPassword,shadowLastChange by self write by dn="cn=admin,dc=example,dc=com" manage by dn.exact="cn=read-only-admin,dc=example,dc=com" manage by anonymous auth by * none
20
+ olcAccess: to dn.subtree="ou=groups,dc=example,dc=com" by users read by dn.exact="cn=read-only-admin,dc=example,dc=com" read by dn="cn=admin,dc=example,dc=com" manage
21
+ olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by dn="cn=admin,dc=example,dc=com" manage by dn.exact="cn=read-only-admin,dc=example,dc=com" manage by users read by self read by * none
@@ -0,0 +1,7 @@
1
+ FROM osixia/openldap:1.5.0
2
+
3
+ # Seed the test directory (dc=example,dc=com) with the users, groups and
4
+ # admin account used by the integration tests, and grant the test admin
5
+ # (cn=read-only-admin) the rights it needs. Files are applied by the
6
+ # osixia/openldap bootstrap at the container's first start.
7
+ COPY docker/ldap/10-ldap-test-data.ldif docker/ldap/20-ldap-test-acl.ldif /container/service/slapd/assets/config/bootstrap/ldif/custom/
@@ -1,13 +1,15 @@
1
1
  services:
2
2
  ldap:
3
- image: symas/openldap:2.6
3
+ # LDAP server used by the integration tests, based on osixia/openldap.
4
+ # The Dockerfile in docker/ldap/ seeds the directory with the users,
5
+ # groups and admin account the tests rely on.
6
+ build:
7
+ context: .
8
+ dockerfile: docker/ldap/Dockerfile
4
9
  ports:
5
- - '1389:1389'
6
- - '1636:1636'
10
+ - '1389:389'
11
+ - '1636:636'
7
12
  environment:
8
- - LDAP_ROOT=dc=example,dc=com
9
- - LDAP_ADMIN_USERNAME=read-only-admin
13
+ - LDAP_DOMAIN=example.com
10
14
  - LDAP_ADMIN_PASSWORD=password
11
- - LDAP_USERS=gauss,einstein
12
- - LDAP_PASSWORDS=password,password
13
- - LDAP_GROUP=科学A部
15
+ - LDAP_TLS=true
package/index.d.ts CHANGED
@@ -8,6 +8,7 @@ declare module 'ldap-authentication' {
8
8
  adminPassword?: string
9
9
  userSearchBase?: string
10
10
  usernameAttribute?: string
11
+ usernameFilter?:string
11
12
  username?: string
12
13
  verifyUserExists?: boolean
13
14
  starttls?: boolean
@@ -36,9 +37,37 @@ declare module 'ldap-authentication' {
36
37
  readonly client: any
37
38
  }
38
39
 
40
+ export interface FetchUsersOptions {
41
+ ldapOpts: ClientOptions
42
+ adminDn: string
43
+ adminPassword: string
44
+ userSearchBase: string
45
+ /**
46
+ * LDAP search filter used to select the users to return.
47
+ * Defaults to `(|(uid=*)(sAMAccountName=*))`, which matches both
48
+ * POSIX (`uid`) and Active Directory (`sAMAccountName`) users.
49
+ * Example: `'(objectClass=person)'`, or `'(objectClass=*)'` to match everything.
50
+ */
51
+ userFilter?: string
52
+ /** A list of attributes of the users to be returned from the LDAP server. If omitted, all details will be returned. */
53
+ attributes?: string[]
54
+ /** A list of attributes to be returned as base64-encoded strings. */
55
+ explicitBufferAttributes?: string[]
56
+ /** Number of entries to fetch per page for the paged search. Defaults to 1000. */
57
+ pageSize?: number
58
+ starttls?: boolean
59
+ }
60
+
39
61
  export function authenticate(options: AuthenticationOptions): Promise<any>
40
62
  export function authenticateResult(options: AuthenticationOptions): Promise<AuthenticationResult>
41
63
 
64
+ /**
65
+ * Bind with the admin account and search all users under `userSearchBase`.
66
+ * The search always uses paged results, so results are not limited by the
67
+ * common server-side limit of 1000 entries.
68
+ */
69
+ export function fetchUsers(options: FetchUsersOptions): Promise<any[]>
70
+
42
71
  export class LdapAuthenticationError extends Error {
43
72
  constructor(message: any)
44
73
  name: string
package/index.js CHANGED
@@ -45,6 +45,9 @@ const AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS = -2
45
45
  const AUTH_RESULT_FAILURE_CREDENTIAL_INVALID = -3
46
46
  const AUTH_RESULT_FAILURE_UNCATEGORIZED = -4
47
47
 
48
+ const DEFAULT_FETCH_USERS_FILTER = '(|(uid=*)(sAMAccountName=*))'
49
+ const DEFAULT_FETCH_USERS_PAGE_SIZE = 1000
50
+
48
51
  class AuthenticationResult {
49
52
  #authCode = AUTH_RESULT_FAILURE_UNCATEGORIZED
50
53
  #identity
@@ -124,19 +127,30 @@ async function _ldapBind(dn, password, starttls, ldapOpts) {
124
127
  return client
125
128
  }
126
129
 
130
+ // replace username in filter
131
+
132
+
127
133
  // search a user and return the object
128
134
  async function _searchUser(
129
135
  ldapClient,
130
136
  searchBase,
137
+ usernameFilter,
131
138
  usernameAttribute,
132
139
  username,
133
140
  attributes = null,
134
141
  explicitBufferAttributes = null
135
142
  ) {
136
- let filter = new ldapts.EqualityFilter({
137
- attribute: usernameAttribute,
138
- value: username,
139
- })
143
+ let filter;
144
+ if(usernameFilter){
145
+ filter = usernameFilter.replaceAll("{{username}}",username.replaceAll(/[&|!*()]/g,""));
146
+ }
147
+ else{
148
+ filter = new ldapts.EqualityFilter({
149
+ attribute: usernameAttribute,
150
+ value: username,
151
+ })
152
+ }
153
+
140
154
  let searchOptions = {
141
155
  filter: filter,
142
156
  scope: 'sub',
@@ -262,10 +276,60 @@ async function _searchUserGroups(
262
276
  return groups
263
277
  }
264
278
 
279
+ // search all users under the search base and return the list of user objects
280
+ async function _fetchAllUsers(
281
+ ldapClient,
282
+ searchBase,
283
+ userFilter,
284
+ attributes = null,
285
+ explicitBufferAttributes = null,
286
+ pageSize = DEFAULT_FETCH_USERS_PAGE_SIZE
287
+ ) {
288
+ let filter = userFilter || DEFAULT_FETCH_USERS_FILTER
289
+
290
+ let searchOptions = {
291
+ filter: filter,
292
+ scope: 'sub',
293
+ // always use paged results, so more than the usual server-side limit
294
+ // (usually 1000 entries per page) can be returned
295
+ paged: { pageSize: pageSize },
296
+ }
297
+ if (attributes) {
298
+ searchOptions.attributes = attributes
299
+ }
300
+ if (explicitBufferAttributes) {
301
+ searchOptions.explicitBufferAttributes = explicitBufferAttributes
302
+ }
303
+
304
+ const { searchEntries } = await ldapClient.search(searchBase, searchOptions)
305
+
306
+ let users = searchEntries || []
307
+ // when attribute endwith ;binary, ldapts returns Buffer, we convert them into base64 string
308
+ for (let user of users) {
309
+ if (user != null && attributes != null) {
310
+ for (let attr of attributes) {
311
+ if (attr.endsWith(';binary') && Buffer.isBuffer(user[attr])) {
312
+ user[attr] = user[attr].toString('base64')
313
+ }
314
+ }
315
+ }
316
+ // when attribute is one of the explicitBufferAttributes, should convert to base64 string
317
+ if (user != null && explicitBufferAttributes != null) {
318
+ for (let attr of explicitBufferAttributes) {
319
+ if (Buffer.isBuffer(user[attr])) {
320
+ user[attr] = user[attr].toString('base64')
321
+ }
322
+ }
323
+ }
324
+ }
325
+ return users
326
+ }
327
+
265
328
  async function authenticateWithAdmin(
266
329
  adminDn,
267
330
  adminPassword,
268
331
  userSearchBase,
332
+ usernameFilter,
269
333
  usernameAttribute,
270
334
  username,
271
335
  userPassword,
@@ -303,6 +367,7 @@ async function authenticateWithAdmin(
303
367
  let searchResult = await _searchUser(
304
368
  ldapAdminClient,
305
369
  userSearchBase,
370
+ usernameFilter,
306
371
  usernameAttribute,
307
372
  username,
308
373
  attributes,
@@ -368,6 +433,7 @@ async function authenticateWithAdmin(
368
433
  async function authenticateWithUser(
369
434
  userDn,
370
435
  userSearchBase,
436
+ usernameFilter,
371
437
  usernameAttribute,
372
438
  username,
373
439
  userPassword,
@@ -411,6 +477,7 @@ async function authenticateWithUser(
411
477
  let searchResult = await _searchUser(
412
478
  ldapUserClient,
413
479
  userSearchBase,
480
+ usernameFilter,
414
481
  usernameAttribute,
415
482
  username,
416
483
  attributes,
@@ -463,6 +530,7 @@ async function verifyUserExists(
463
530
  adminDn,
464
531
  adminPassword,
465
532
  userSearchBase,
533
+ usernameFilter,
466
534
  usernameAttribute,
467
535
  username,
468
536
  starttls,
@@ -498,6 +566,7 @@ async function verifyUserExists(
498
566
  let searchResult = await _searchUser(
499
567
  ldapAdminClient,
500
568
  userSearchBase,
569
+ usernameFilter,
501
570
  usernameAttribute,
502
571
  username,
503
572
  attributes,
@@ -544,6 +613,51 @@ async function verifyUserExists(
544
613
  )
545
614
  }
546
615
 
616
+ // fetch all users under the search base, using the admin account to search.
617
+ // the search always uses paged results so the common server-side limit of
618
+ // 1000 entries does not apply.
619
+ async function fetchUsers(options) {
620
+ assert(
621
+ options.ldapOpts && options.ldapOpts.url,
622
+ 'fetchUsers: ldapOpts.url must be provided'
623
+ )
624
+ assert(options.adminDn, 'fetchUsers: adminDn must be provided')
625
+ assert(options.adminPassword, 'fetchUsers: adminPassword must be provided')
626
+ assert(options.userSearchBase, 'fetchUsers: userSearchBase must be provided')
627
+
628
+ let ldapAdminClient
629
+ try {
630
+ ldapAdminClient = await _ldapBind(
631
+ options.adminDn,
632
+ options.adminPassword,
633
+ options.starttls,
634
+ options.ldapOpts
635
+ )
636
+ } catch (error) {
637
+ if (ldapAdminClient && ldapAdminClient.isConnected) {
638
+ await ldapAdminClient.unbind()
639
+ }
640
+ throw new LdapAuthenticationError(error.message || 'admin bind failed')
641
+ }
642
+
643
+ try {
644
+ return await _fetchAllUsers(
645
+ ldapAdminClient,
646
+ options.userSearchBase,
647
+ options.userFilter,
648
+ options.attributes,
649
+ options.explicitBufferAttributes,
650
+ options.pageSize || DEFAULT_FETCH_USERS_PAGE_SIZE
651
+ )
652
+ } catch (error) {
653
+ throw new LdapAuthenticationError(error.message || 'user search failed')
654
+ } finally {
655
+ if (ldapAdminClient && ldapAdminClient.isConnected) {
656
+ await ldapAdminClient.unbind()
657
+ }
658
+ }
659
+ }
660
+
547
661
  async function authenticate(options) {
548
662
  const result = await authenticateResult(options)
549
663
 
@@ -562,8 +676,8 @@ async function authenticateResult(options) {
562
676
  assert(options.adminPassword, 'Admin mode adminPassword must be provided')
563
677
  assert(options.userSearchBase, 'Admin mode userSearchBase must be provided')
564
678
  assert(
565
- options.usernameAttribute,
566
- 'Admin mode usernameAttribute must be provided'
679
+ options.usernameAttribute || options.usernameFilter,
680
+ 'Admin mode usernameAttribute or usernameFilter must be provided'
567
681
  )
568
682
  assert(options.username, 'Admin mode username must be provided')
569
683
  } else {
@@ -584,6 +698,7 @@ async function authenticateResult(options) {
584
698
  options.adminDn,
585
699
  options.adminPassword,
586
700
  options.userSearchBase,
701
+ options.usernameFilter,
587
702
  options.usernameAttribute,
588
703
  options.username,
589
704
  options.starttls,
@@ -607,6 +722,7 @@ async function authenticateResult(options) {
607
722
  options.adminDn,
608
723
  options.adminPassword,
609
724
  options.userSearchBase,
725
+ options.usernameFilter,
610
726
  options.usernameAttribute,
611
727
  options.username,
612
728
  options.userPassword,
@@ -625,6 +741,7 @@ async function authenticateResult(options) {
625
741
  return await authenticateWithUser(
626
742
  options.userDn,
627
743
  options.userSearchBase,
744
+ options.usernameFilter,
628
745
  options.usernameAttribute,
629
746
  options.username,
630
747
  options.userPassword,
@@ -666,6 +783,7 @@ module.exports.AuthenticationResult = AuthenticationResult
666
783
 
667
784
  module.exports.authenticate = authenticate
668
785
  module.exports.authenticateResult = authenticateResult
786
+ module.exports.fetchUsers = fetchUsers
669
787
  module.exports.LdapAuthenticationError = LdapAuthenticationError
670
788
 
671
789
  module.exports.exportForTesting = {
package/index.mjs ADDED
@@ -0,0 +1,17 @@
1
+ import ldapAuthentication from './index.js'
2
+
3
+ export {
4
+ AUTH_RESULT_FAILURE,
5
+ AUTH_RESULT_FAILURE_CREDENTIAL_INVALID,
6
+ AUTH_RESULT_FAILURE_IDENTITY_AMBIGUOUS,
7
+ AUTH_RESULT_FAILURE_IDENTITY_NOT_FOUND,
8
+ AUTH_RESULT_FAILURE_UNCATEGORIZED,
9
+ AUTH_RESULT_SUCCESS,
10
+ AuthenticationResult,
11
+ LdapAuthenticationError,
12
+ authenticate,
13
+ authenticateResult,
14
+ fetchUsers,
15
+ } from './index.js'
16
+
17
+ export default ldapAuthentication
package/package.json CHANGED
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "name": "ldap-authentication",
3
- "version": "4.0.4",
3
+ "version": "4.1.0",
4
4
  "description": "A simple async nodejs library for LDAP user authentication",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "import": "./index.mjs",
11
+ "require": "./index.js",
12
+ "default": "./index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
7
16
  "engines": {
8
17
  "node": ">=20.0.0"
9
18
  },
@@ -0,0 +1,136 @@
1
+ const { fetchUsers, LdapAuthenticationError } = require('../index.js')
2
+
3
+ const url = process.env.INGITHUB ? 'ldap://localhost:1389' : 'ldap://ldap:1389'
4
+
5
+ describe('ldap-authentication fetchUsers test', () => {
6
+ const baseOptions = {
7
+ ldapOpts: {
8
+ url: url,
9
+ },
10
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
11
+ adminPassword: 'password',
12
+ userSearchBase: 'dc=example,dc=com',
13
+ }
14
+
15
+ it('Should return all users with the default filter', async () => {
16
+ let users = await fetchUsers(baseOptions)
17
+
18
+ expect(Array.isArray(users)).toBe(true)
19
+ expect(users.length).toBe(2)
20
+ let uids = users.map((user) => user.uid)
21
+ expect(uids).toContain('gauss')
22
+ expect(uids).toContain('einstein')
23
+ for (let user of users) {
24
+ expect(user.dn).toBeTruthy()
25
+ }
26
+ })
27
+
28
+ it('Should return only the requested attributes', async () => {
29
+ let users = await fetchUsers({
30
+ ...baseOptions,
31
+ attributes: ['uid', 'sn'],
32
+ })
33
+
34
+ expect(users.length).toBe(2)
35
+ let gauss = users.find((user) => user.uid === 'gauss')
36
+ expect(gauss).toBeTruthy()
37
+ expect(gauss.sn).toEqual('Bar1')
38
+ expect(gauss.cn).toBeUndefined()
39
+ })
40
+
41
+ it('Should return only matching users with a custom userFilter', async () => {
42
+ let users = await fetchUsers({
43
+ ...baseOptions,
44
+ userFilter: '(uid=gauss)',
45
+ })
46
+
47
+ expect(users.length).toBe(1)
48
+ expect(users[0].uid).toEqual('gauss')
49
+ expect(users[0].sn).toEqual('Bar1')
50
+ })
51
+
52
+ it('Should return all entries including non-users when userFilter matches everything', async () => {
53
+ let users = await fetchUsers({
54
+ ...baseOptions,
55
+ userFilter: '(objectClass=*)',
56
+ })
57
+
58
+ expect(users.length).toBeGreaterThan(2)
59
+ let dns = users.map((user) => user.dn)
60
+ expect(dns).toContain('cn=gauss,ou=users,dc=example,dc=com')
61
+ expect(dns).toContain('cn=科学A部,ou=groups,dc=example,dc=com')
62
+ })
63
+
64
+ it('Should return empty list when userFilter matches nothing', async () => {
65
+ let users = await fetchUsers({
66
+ ...baseOptions,
67
+ userFilter: '(uid=does-not-exist)',
68
+ })
69
+
70
+ expect(users).toEqual([])
71
+ })
72
+ })
73
+
74
+ describe('ldap-authentication fetchUsers negative test', () => {
75
+ const baseOptions = {
76
+ ldapOpts: {
77
+ url: url,
78
+ },
79
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
80
+ adminPassword: 'password',
81
+ userSearchBase: 'dc=example,dc=com',
82
+ }
83
+
84
+ it('wrong admin password should throw LdapAuthenticationError', async () => {
85
+ let options = {
86
+ ...baseOptions,
87
+ adminPassword: 'wrongpassword',
88
+ }
89
+
90
+ let e = null
91
+ try {
92
+ await fetchUsers(options)
93
+ } catch (error) {
94
+ e = error
95
+ }
96
+
97
+ expect(e).toBeTruthy()
98
+ expect(e).toBeInstanceOf(LdapAuthenticationError)
99
+ })
100
+
101
+ it('wrong admin dn should throw LdapAuthenticationError', async () => {
102
+ let options = {
103
+ ...baseOptions,
104
+ adminDn: 'cn=not-exist,dc=example,dc=com',
105
+ }
106
+
107
+ let e = null
108
+ try {
109
+ await fetchUsers(options)
110
+ } catch (error) {
111
+ e = error
112
+ }
113
+
114
+ expect(e).toBeTruthy()
115
+ expect(e).toBeInstanceOf(LdapAuthenticationError)
116
+ })
117
+
118
+ it('missing userSearchBase should throw', async () => {
119
+ let options = {
120
+ ldapOpts: {
121
+ url: url,
122
+ },
123
+ adminDn: 'cn=read-only-admin,dc=example,dc=com',
124
+ adminPassword: 'password',
125
+ }
126
+
127
+ let e = null
128
+ try {
129
+ await fetchUsers(options)
130
+ } catch (error) {
131
+ e = error
132
+ }
133
+
134
+ expect(e).toBeTruthy()
135
+ })
136
+ })