@@ -76,6 +76,7 @@ describe(AuthProviderLDAP.name, () => {
7676 configuration . auth . ldap = next
7777 ; ( authProviderLDAP as any ) . ldapConfig = next
7878 ; ( authProviderLDAP as any ) . isAD = [ LDAP_LOGIN_ATTR . SAM , LDAP_LOGIN_ATTR . UPN ] . includes ( next . attributes . login )
79+ ; ( authProviderLDAP as any ) . hasServiceBind = Boolean ( next . serviceBindDN && next . serviceBindPassword )
7980 }
8081
8182 const mockBindResolve = ( ) => {
@@ -306,6 +307,120 @@ describe(AuthProviderLDAP.name, () => {
306307 expect ( usersManager . updateAccesses ) . toHaveBeenCalledWith ( createdUser , '192.168.1.10' , true )
307308 } )
308309
310+ it ( 'should accept adminGroup as full DN' , async ( ) => {
311+ setLdapConfig ( {
312+ options : {
313+ adminGroup : 'CN=Admins,OU=Groups,DC=example,DC=org'
314+ }
315+ } )
316+ usersManager . findUser . mockResolvedValue ( null )
317+ mockBindResolve ( )
318+ mockSearchEntries ( [
319+ {
320+ uid : 'john' ,
321+ givenName : 'John' ,
322+ sn : 'Doe' ,
323+ mail : 'john@example.org' ,
324+ memberOf : [ 'CN=Admins,OU=Groups,DC=example,DC=org' ]
325+ }
326+ ] )
327+ const createdUser : any = { id : 9 , login : 'john' , isGuest : false , isActive : true , makePaths : jest . fn ( ) }
328+ adminUsersManager . createUserOrGuest . mockResolvedValue ( createdUser )
329+ usersManager . fromUserId . mockResolvedValue ( createdUser )
330+
331+ const res = await authProviderLDAP . validateUser ( 'john' , 'pwd' )
332+
333+ expect ( adminUsersManager . createUserOrGuest ) . toHaveBeenCalledWith (
334+ expect . objectContaining ( { role : USER_ROLE . ADMINISTRATOR } ) ,
335+ USER_ROLE . ADMINISTRATOR
336+ )
337+ expect ( res ) . toBe ( createdUser )
338+ } )
339+
340+ it ( 'should use groupOfNames to detect admin membership when memberOf is missing' , async ( ) => {
341+ setLdapConfig ( { options : { adminGroup : 'Admins' } } )
342+ usersManager . findUser . mockResolvedValue ( null )
343+ mockBindResolve ( )
344+ ldapClient . search
345+ . mockResolvedValueOnce ( {
346+ searchEntries : [
347+ {
348+ uid : 'john' ,
349+ cn : 'John Doe' ,
350+ mail : 'john@example.org' ,
351+ dn : 'uid=john,ou=people,dc=example,dc=org'
352+ }
353+ ]
354+ } )
355+ . mockResolvedValueOnce ( { searchEntries : [ { cn : 'Admins' } ] } )
356+ const createdUser : any = { id : 3 , login : 'john' , isGuest : false , isActive : true , makePaths : jest . fn ( ) }
357+ adminUsersManager . createUserOrGuest . mockResolvedValue ( createdUser )
358+ usersManager . fromUserId . mockResolvedValue ( createdUser )
359+
360+ const res = await authProviderLDAP . validateUser ( 'john' , 'pwd' )
361+
362+ expect ( adminUsersManager . createUserOrGuest ) . toHaveBeenCalledWith (
363+ expect . objectContaining ( { role : USER_ROLE . ADMINISTRATOR } ) ,
364+ USER_ROLE . ADMINISTRATOR
365+ )
366+ expect ( res ) . toBe ( createdUser )
367+ } )
368+
369+ it ( 'should use service bind for LDAP searches when configured' , async ( ) => {
370+ setLdapConfig ( {
371+ serviceBindDN : 'cn=svc,dc=example,dc=org' ,
372+ serviceBindPassword : 'secret'
373+ } )
374+ usersManager . findUser . mockResolvedValue ( null )
375+ mockBindResolve ( )
376+ ldapClient . search . mockResolvedValueOnce ( {
377+ searchEntries : [ { uid : 'john' , cn : 'John Doe' , mail : 'john@example.org' , dn : 'uid=john,ou=people,dc=example,dc=org' } ]
378+ } )
379+ const createdUser : any = { id : 8 , login : 'john' , isGuest : false , isActive : true , makePaths : jest . fn ( ) }
380+ adminUsersManager . createUserOrGuest . mockResolvedValue ( createdUser )
381+ usersManager . fromUserId . mockResolvedValue ( createdUser )
382+
383+ await authProviderLDAP . validateUser ( 'john' , 'pwd' )
384+
385+ expect ( ldapClient . bind ) . toHaveBeenCalledWith ( 'cn=svc,dc=example,dc=org' , 'secret' )
386+ expect ( ldapClient . bind ) . toHaveBeenCalledWith ( 'uid=john,ou=people,dc=example,dc=org' , 'pwd' )
387+ } )
388+
389+ it ( 'should return null when service bind is set but user DN is not found' , async ( ) => {
390+ setLdapConfig ( {
391+ serviceBindDN : 'cn=svc,dc=example,dc=org' ,
392+ serviceBindPassword : 'secret'
393+ } )
394+ usersManager . findUser . mockResolvedValue ( null )
395+ mockBindResolve ( )
396+ ldapClient . search . mockResolvedValueOnce ( { searchEntries : [ ] } )
397+
398+ const res = await authProviderLDAP . validateUser ( 'john' , 'pwd' )
399+
400+ expect ( res ) . toBeNull ( )
401+ expect ( ldapClient . bind ) . toHaveBeenCalledWith ( 'cn=svc,dc=example,dc=org' , 'secret' )
402+ expect ( ldapClient . bind ) . not . toHaveBeenCalledWith ( 'uid=john,ou=people,dc=example,dc=org' , 'pwd' )
403+ } )
404+
405+ it ( 'should return null when user bind fails after service bind' , async ( ) => {
406+ setLdapConfig ( {
407+ serviceBindDN : 'cn=svc,dc=example,dc=org' ,
408+ serviceBindPassword : 'secret'
409+ } )
410+ usersManager . findUser . mockResolvedValue ( null )
411+ ldapClient . unbind . mockResolvedValue ( undefined )
412+ ldapClient . bind . mockResolvedValueOnce ( undefined ) . mockRejectedValueOnce ( new InvalidCredentialsError ( 'invalid credentials' ) )
413+ ldapClient . search . mockResolvedValueOnce ( {
414+ searchEntries : [ { dn : 'uid=john,ou=people,dc=example,dc=org' , cn : 'John Doe' } ]
415+ } )
416+
417+ const res = await authProviderLDAP . validateUser ( 'john' , 'pwd' )
418+
419+ expect ( res ) . toBeNull ( )
420+ expect ( ldapClient . bind ) . toHaveBeenCalledWith ( 'cn=svc,dc=example,dc=org' , 'secret' )
421+ expect ( ldapClient . bind ) . toHaveBeenCalledWith ( 'uid=john,ou=people,dc=example,dc=org' , 'pwd' )
422+ } )
423+
309424 it ( 'should keep admin role when adminGroup is not configured' , async ( ) => {
310425 setLdapConfig ( { options : { adminGroup : undefined } } )
311426 const existingUser : any = buildUser ( { id : 5 , role : USER_ROLE . ADMINISTRATOR } )
@@ -388,7 +503,7 @@ describe(AuthProviderLDAP.name, () => {
388503
389504 expect ( normalized . uid ) . toBe ( 'john' )
390505 expect ( normalized . mail ) . toBe ( 'john@example.org' )
391- expect ( normalized . memberOf ) . toEqual ( [ 'Admins' , 'Staff' ] )
506+ expect ( normalized . memberOf ) . toEqual ( [ 'CN= Admins,OU=Groups,DC=example,DC=org' , 'Admins' , 'CN=Staff,OU=Groups,DC=example,DC=org ', 'Staff' ] )
392507 } )
393508
394509 it ( 'should build LDAP logins for SAM account name when netbiosName is set' , ( ) => {
0 commit comments