diff --git a/src/core/ldap/domain/ldap-client.service.spec.ts b/src/core/ldap/domain/ldap-client.service.spec.ts index 07bed06a5..2d100f5e6 100644 --- a/src/core/ldap/domain/ldap-client.service.spec.ts +++ b/src/core/ldap/domain/ldap-client.service.spec.ts @@ -28,8 +28,6 @@ import { LdapEmailAddressError } from '../error/ldap-email-address.error.js'; import { LdapCreateLehrerError } from '../error/ldap-create-lehrer.error.js'; import { LdapModifyEmailError } from '../error/ldap-modify-email.error.js'; import { LdapInstanceConfig } from '../ldap-instance-config.js'; -import { LdapAddPersonToGroupError } from '../error/ldap-add-person-to-group.error.js'; -import { LdapRemovePersonFromGroupError } from '../error/ldap-remove-person-from-group.error.js'; import { LdapModifyUserPasswordError } from '../error/ldap-modify-user-password.error.js'; describe('LDAP Client Service', () => { @@ -46,14 +44,6 @@ describe('LDAP Client Service', () => { let person: Person; let personWithoutReferrer: Person; - const mockLdapInstanceConfig: LdapInstanceConfig = { - BASE_DN: 'dc=example,dc=com', - OEFFENTLICHE_SCHULEN_DOMAIN: 'schule-sh.de', - ERSATZSCHULEN_DOMAIN: 'ersatzschule-sh.de', - URL: '', - BIND_DN: '', - ADMIN_PASSWORD: '', - }; beforeAll(async () => { module = await Test.createTestingModule({ @@ -62,17 +52,12 @@ describe('LDAP Client Service', () => { DatabaseTestModule.forRoot({ isDatabaseRequired: true }), LdapModule, MapperTestModule, - LdapConfigModule, ], providers: [ { provide: APP_PIPE, useClass: GlobalValidationPipe, }, - { - provide: LdapInstanceConfig, - useValue: mockLdapInstanceConfig, - }, ], }) .overrideModule(LdapConfigModule) @@ -83,8 +68,6 @@ describe('LDAP Client Service', () => { .useValue(createMock()) .overrideProvider(EventService) .useValue(createMock()) - .overrideProvider(LdapInstanceConfig) - .useValue(mockLdapInstanceConfig) .compile(); orm = module.get(MikroORM); @@ -139,236 +122,6 @@ describe('LDAP Client Service', () => { it('should be defined', () => { expect(em).toBeDefined(); }); - describe('updateMemberDnInGroups', () => { - const fakeOldReferrer: string = 'old-user'; - const fakeNewReferrer: string = 'new-user'; - const fakeOldReferrerUid: string = `uid=${fakeOldReferrer},ou=users,${mockLdapInstanceConfig.BASE_DN}`; - const fakeNewReferrerUid: string = `uid=${fakeNewReferrer},ou=users,${mockLdapInstanceConfig.BASE_DN}`; - const fakeGroupDn: string = 'cn=lehrer-group,' + mockLdapInstanceConfig.BASE_DN; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should update member DN in all groups successfully', async () => { - const clientMock2: Client = { - search: jest.fn().mockResolvedValueOnce({ - searchEntries: [ - { - dn: fakeGroupDn, - member: [fakeOldReferrerUid, 'uid=other-user,ou=users,' + mockLdapInstanceConfig.BASE_DN], - }, - ], - searchReferences: [], - }), - modify: jest.fn().mockResolvedValueOnce({}), - } as unknown as Client; - - const result: Result = await ldapClientService.updateMemberDnInGroups( - fakeOldReferrer, - fakeNewReferrer, - fakeOldReferrerUid, - clientMock2, - ); - expect(result.ok).toBeTruthy(); - if (!result.ok) throw Error(); - expect(result.value).toBe(`Updated member data for 1 groups.`); - expect(clientMock2.modify).toHaveBeenCalledTimes(1); - expect(clientMock2.modify).toHaveBeenNthCalledWith(1, fakeGroupDn, [ - new Change({ - operation: 'replace', - modification: new Attribute({ - type: 'member', - values: [fakeNewReferrerUid, 'uid=other-user,ou=users,' + mockLdapInstanceConfig.BASE_DN], - }), - }), - ]); - expect(loggerMock.info).toHaveBeenCalledWith(`LDAP: Updated member data for group: ${fakeGroupDn}`); - }); - - it('should return a message when no groups are found', async () => { - const clientMock3: Client = { - search: jest.fn().mockResolvedValueOnce({ - searchEntries: [], - searchReferences: [], - }), - modify: jest.fn().mockResolvedValueOnce({}), - } as unknown as Client; - - const result: Result = await ldapClientService.updateMemberDnInGroups( - fakeOldReferrer, - fakeNewReferrer, - fakeOldReferrerUid, - clientMock3, - ); - - expect(result.ok).toBeTruthy(); - if (!result.ok) throw Error(); - expect(result.value).toBe(`No groups found for person:${fakeOldReferrer}`); - expect(loggerMock.info).toHaveBeenCalledWith(`LDAP: No groups found for person:${fakeOldReferrer}`); - }); - - it('should handle errors when updating group membership fails', async () => { - const clientMock5: Client = { - search: jest.fn().mockResolvedValueOnce({ - searchEntries: [ - { - dn: fakeGroupDn, - member: [fakeOldReferrerUid], - }, - ], - searchReferences: [], - }), - modify: jest.fn().mockRejectedValueOnce(new Error('Modify error')), - } as unknown as Client; - - const result: Result = await ldapClientService.updateMemberDnInGroups( - fakeOldReferrer, - fakeNewReferrer, - fakeOldReferrerUid, - clientMock5, - ); - - expect(result.ok).toBeTruthy(); - expect(loggerMock.error).toHaveBeenCalledWith( - `LDAP: Error while updating member data for group: ${fakeGroupDn}, errMsg: ${String(new Error('Modify error'))}`, - ); - }); - - it('should handle groups with empty or undefined member lists', async () => { - const clientMock4: Client = { - search: jest.fn().mockResolvedValueOnce({ - searchEntries: undefined, - searchReferences: [], - }), - modify: jest.fn().mockResolvedValueOnce({}), - } as unknown as Client; - - const result: Result = await ldapClientService.updateMemberDnInGroups( - fakeOldReferrer, - fakeNewReferrer, - fakeOldReferrerUid, - clientMock4, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error.message).toBe(`LDAP: Error while searching for groups for person: ${fakeOldReferrer}`); - expect(clientMock.modify).not.toHaveBeenCalled(); - expect(loggerMock.error).toHaveBeenCalledWith( - `LDAP: Error while searching for groups for person: ${fakeOldReferrer}`, - ); - }); - - it('should handle member as Buffer correctly', async () => { - const bufferMember: Buffer = Buffer.from(fakeOldReferrerUid); - - clientMock.search.mockResolvedValueOnce({ - searchEntries: [ - { - dn: fakeGroupDn, - member: bufferMember, - }, - ], - searchReferences: [], - }); - - clientMock.modify.mockResolvedValueOnce(); - - const result: Result = await ldapClientService.updateMemberDnInGroups( - fakeOldReferrer, - fakeNewReferrer, - fakeOldReferrerUid, - clientMock, - ); - - expect(result.ok).toBeTruthy(); - if (!result.ok) throw Error(); - expect(result.value).toBe(`Updated member data for 1 groups.`); - expect(clientMock.modify).toHaveBeenCalledWith(fakeGroupDn, [ - new Change({ - operation: 'replace', - modification: new Attribute({ - type: 'member', - values: [fakeNewReferrerUid], - }), - }), - ]); - }); - - it('should handle member as a single string correctly', async () => { - clientMock.search.mockResolvedValueOnce({ - searchEntries: [ - { - dn: fakeGroupDn, - member: fakeOldReferrerUid, - }, - ], - searchReferences: [], - }); - - clientMock.modify.mockResolvedValueOnce(); - - const result: Result = await ldapClientService.updateMemberDnInGroups( - fakeOldReferrer, - fakeNewReferrer, - fakeOldReferrerUid, - clientMock, - ); - - expect(result.ok).toBeTruthy(); - if (!result.ok) throw Error(); - expect(result.value).toBe(`Updated member data for 1 groups.`); - expect(clientMock.modify).toHaveBeenCalledWith(fakeGroupDn, [ - new Change({ - operation: 'replace', - modification: new Attribute({ - type: 'member', - values: [fakeNewReferrerUid], - }), - }), - ]); - }); - - it('should handle member as an array of Buffers correctly', async () => { - const bufferMembers: Buffer[] = [ - Buffer.from(fakeOldReferrerUid), - Buffer.from('uid=other-user,ou=users,' + mockLdapInstanceConfig.BASE_DN), - ]; - - clientMock.search.mockResolvedValueOnce({ - searchEntries: [ - { - dn: fakeGroupDn, - member: bufferMembers, - }, - ], - searchReferences: [], - }); - - clientMock.modify.mockResolvedValueOnce(); - - const result: Result = await ldapClientService.updateMemberDnInGroups( - fakeOldReferrer, - fakeNewReferrer, - fakeOldReferrerUid, - clientMock, - ); - - expect(result.ok).toBeTruthy(); - if (!result.ok) throw Error(); - expect(result.value).toBe(`Updated member data for 1 groups.`); - expect(clientMock.modify).toHaveBeenCalledWith(fakeGroupDn, [ - new Change({ - operation: 'replace', - modification: new Attribute({ - type: 'member', - values: [fakeNewReferrerUid, 'uid=other-user,ou=users,' + mockLdapInstanceConfig.BASE_DN], - }), - }), - ]); - }); - }); describe('getRootName', () => { it('when emailDomain is neither schule-sh.de nor ersatzschule-sh.de should return LdapEmailDomainError', async () => { @@ -477,240 +230,34 @@ describe('LDAP Client Service', () => { }); }); - describe('addPersonToGroup', () => { - const fakeReferrer: string = 'test-user'; - const fakeSchoolReferrer: string = '123'; - const fakeLehrerUid: string = `uid=${fakeReferrer},ou=oeffentlicheSchulen,${mockLdapInstanceConfig.BASE_DN}`; - const fakeGroupId: string = `lehrer-${fakeSchoolReferrer}`; - const fakeGroupDn: string = `cn=${fakeGroupId},cn=groups,ou=${fakeSchoolReferrer},${mockLdapInstanceConfig.BASE_DN}`; - - it('should successfully add a person to an existing group', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalUnit - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalRole - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for groupOfNames - ); - clientMock.modify.mockResolvedValueOnce(); - - return clientMock; - }); - - const result: Result = await ldapClientService.addPersonToGroup( - fakeReferrer, - fakeSchoolReferrer, - fakeLehrerUid, - ); - - expect(result.ok).toBeTruthy(); - expect(clientMock.modify).toHaveBeenCalledWith(fakeGroupDn, [ - new Change({ - operation: 'add', - modification: new Attribute({ - type: 'member', - values: [fakeLehrerUid], - }), - }), - ]); - expect(loggerMock.info).toHaveBeenCalledWith( - `LDAP: Successfully added person ${fakeReferrer} to group ${fakeGroupId}`, - ); - }); - - it('should create a new organizationalUnit and add the person if the organizationalUnit does not exist', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search - .mockResolvedValueOnce( - createMock({ searchEntries: [] }), // No organizationalUnit found - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalRole - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for groupOfNames - ); - clientMock.add.mockResolvedValueOnce(); // Add organizationalUnit - clientMock.modify.mockResolvedValueOnce(); - - return clientMock; - }); - - const result: Result = await ldapClientService.addPersonToGroup( - fakeReferrer, - fakeSchoolReferrer, - fakeLehrerUid, - ); - - expect(result.ok).toBeTruthy(); - expect(clientMock.add).toHaveBeenCalledWith( - `ou=${fakeSchoolReferrer},${mockLdapInstanceConfig.BASE_DN}`, - expect.objectContaining({ ou: fakeSchoolReferrer, objectClass: 'organizationalUnit' }), - ); - }); - - it('should create a new group and add the person if the group does not exist', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalUnit - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalRole - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [] }), // No groupOfNames found - ); - clientMock.add.mockResolvedValueOnce(); // Add group - clientMock.modify.mockResolvedValueOnce(); - - return clientMock; - }); - - const result: Result = await ldapClientService.addPersonToGroup( - fakeReferrer, - fakeSchoolReferrer, - fakeLehrerUid, - ); - - expect(result.ok).toBeTruthy(); - expect(clientMock.add).toHaveBeenCalledWith(fakeGroupDn, { - cn: fakeGroupId, - objectclass: ['groupOfNames'], - member: [fakeLehrerUid], - }); - expect(loggerMock.info).toHaveBeenCalledWith( - `LDAP: Successfully created group ${fakeGroupId} and added person ${fakeReferrer}`, - ); - }); - - it('should return error if group creation fails', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalUnit - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalRole - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [] }), // No groupOfNames found - ); - clientMock.add.mockRejectedValueOnce(new Error('Group creation failed')); - - return clientMock; - }); - - const result: Result = await ldapClientService.addPersonToGroup( - fakeReferrer, - fakeSchoolReferrer, - fakeLehrerUid, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error).toBeInstanceOf(LdapAddPersonToGroupError); - expect(loggerMock.error).toHaveBeenCalledWith( - `LDAP: Failed to create group ${fakeGroupId}, errMsg: Error: Group creation failed`, - ); - }); - - it('should return error if person addition to the group fails', async () => { + describe('executeWithRetry', () => { + it('when writing operation fails it should automatically retry the operation', async () => { ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalUnit - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Search for organizationalRole - ) - .mockResolvedValueOnce( - createMock({ searchEntries: [createMock()] }), // Group found - ); - clientMock.modify.mockRejectedValueOnce(new Error('Modify error')); - - return clientMock; - }); - - const result: Result = await ldapClientService.addPersonToGroup( - fakeReferrer, - fakeSchoolReferrer, - fakeLehrerUid, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error).toBeInstanceOf(LdapAddPersonToGroupError); - expect(loggerMock.error).toHaveBeenCalledWith( - `LDAP: Failed to add person to group ${fakeGroupId}, errMsg: Error: Modify error`, - ); - }); + clientMock.bind.mockResolvedValue(); + clientMock.add.mockRejectedValue(new Error()); + clientMock.search.mockResolvedValueOnce(createMock()); //mock existsLehrer - it('should return error if bind fails', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockRejectedValueOnce(new Error()); return clientMock; }); - - const result: Result = await ldapClientService.addPersonToGroup( - fakeReferrer, - fakeSchoolReferrer, - fakeLehrerUid, - ); + const testLehrer: PersonData = { + id: faker.string.uuid(), + vorname: faker.person.firstName(), + familienname: faker.person.lastName(), + referrer: faker.lorem.word(), + ldapEntryUUID: faker.string.uuid(), + }; + const result: Result = await ldapClientService.createLehrer(testLehrer, 'schule-sh.de'); expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error).toBeInstanceOf(Error); - }); - - it('should return false if person is already in the group', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search - .mockResolvedValueOnce(createMock({ searchEntries: [createMock()] })) - .mockResolvedValueOnce(createMock({ searchEntries: [createMock()] })) - .mockResolvedValueOnce( - createMock({ - searchEntries: [ - createMock({ - dn: fakeGroupDn, - member: [fakeLehrerUid], - }), - ], - }), - ); - - return clientMock; - }); - - const result: Result = await ldapClientService.addPersonToGroup( - fakeReferrer, - fakeSchoolReferrer, - fakeLehrerUid, - ); - - expect(result.ok).toBeTruthy(); - if (!result.ok) throw Error(); - expect(result.value).toBe(false); - expect(clientMock.modify).not.toHaveBeenCalled(); - expect(loggerMock.info).toHaveBeenCalledWith( - `LDAP: Person ${fakeReferrer} is already in group ${fakeGroupId}`, - ); + expect(clientMock.bind).toHaveBeenCalledTimes(3); + expect(loggerMock.warning).toHaveBeenCalledWith(expect.stringContaining('Attempt 1 failed')); + expect(loggerMock.warning).toHaveBeenCalledWith(expect.stringContaining('Attempt 2 failed')); + expect(loggerMock.warning).toHaveBeenCalledWith(expect.stringContaining('Attempt 3 failed')); }); }); describe('creation', () => { const fakeEmailDomain: string = 'schule-sh.de'; - const fakeOrgaKennung: string = '123'; describe('lehrer', () => { it('when called with extra entryUUID should return truthy result', async () => { @@ -728,13 +275,8 @@ describe('LDAP Client Service', () => { referrer: faker.lorem.word(), ldapEntryUUID: faker.string.uuid(), }; - const lehrerUid: string = - 'uid=' + testLehrer.referrer + ',ou=oeffentlicheSchulen,' + mockLdapInstanceConfig.BASE_DN; - const result: Result = await ldapClientService.createLehrer( - testLehrer, - fakeEmailDomain, - fakeOrgaKennung, - ); + const lehrerUid: string = 'uid=' + testLehrer.referrer + ',ou=oeffentlicheSchulen,dc=schule-sh,dc=de'; + const result: Result = await ldapClientService.createLehrer(testLehrer, fakeEmailDomain); expect(result.ok).toBeTruthy(); expect(loggerMock.info).toHaveBeenLastCalledWith(`LDAP: Successfully created lehrer ${lehrerUid}`); @@ -754,13 +296,8 @@ describe('LDAP Client Service', () => { familienname: faker.person.lastName(), referrer: faker.lorem.word(), }; - const lehrerUid: string = - 'uid=' + testLehrer.referrer + ',ou=oeffentlicheSchulen,' + mockLdapInstanceConfig.BASE_DN; - const result: Result = await ldapClientService.createLehrer( - testLehrer, - fakeEmailDomain, - fakeOrgaKennung, - ); + const lehrerUid: string = 'uid=' + testLehrer.referrer + ',ou=oeffentlicheSchulen,dc=schule-sh,dc=de'; + const result: Result = await ldapClientService.createLehrer(testLehrer, fakeEmailDomain); expect(result.ok).toBeTruthy(); expect(loggerMock.info).toHaveBeenLastCalledWith(`LDAP: Successfully created lehrer ${lehrerUid}`); @@ -769,14 +306,7 @@ describe('LDAP Client Service', () => { it('when adding fails should log error', async () => { ldapClientMock.getClient.mockImplementation(() => { clientMock.bind.mockResolvedValue(); - clientMock.bind.mockResolvedValue(); - clientMock.search.mockResolvedValueOnce(createMock()); - clientMock.add.mockResolvedValueOnce(); - clientMock.add.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce(createMock({ searchEntries: [] })); - clientMock.search.mockResolvedValueOnce(createMock({ searchEntries: [] })); - clientMock.search.mockResolvedValueOnce(createMock({ searchEntries: [] })); - clientMock.add.mockResolvedValueOnce(); + clientMock.search.mockResolvedValueOnce(createMock({ searchEntries: [] })); //mock: lehrer not present clientMock.add.mockRejectedValueOnce(new Error('LDAP-Error')); return clientMock; @@ -787,16 +317,11 @@ describe('LDAP Client Service', () => { familienname: faker.person.lastName(), referrer: faker.lorem.word(), }; - const lehrerUid: string = - 'uid=' + testLehrer.referrer + ',ou=oeffentlicheSchulen,' + mockLdapInstanceConfig.BASE_DN; - const result: Result = await ldapClientService.createLehrer( - testLehrer, - fakeEmailDomain, - fakeOrgaKennung, - ); + const lehrerUid: string = 'uid=' + testLehrer.referrer + ',ou=oeffentlicheSchulen,dc=schule-sh,dc=de'; + const result: Result = await ldapClientService.createLehrer(testLehrer, fakeEmailDomain); if (result.ok) throw Error(); - expect(loggerMock.error).toHaveBeenLastCalledWith( + expect(loggerMock.error).toHaveBeenCalledWith( `LDAP: Creating lehrer FAILED, uid:${lehrerUid}, errMsg:{}`, ); expect(result.error).toEqual(new LdapCreateLehrerError()); @@ -818,12 +343,10 @@ describe('LDAP Client Service', () => { ldapEntryUUID: faker.string.uuid(), }; const fakeErsatzSchuleAddressDomain: string = 'ersatzschule-sh.de'; - const lehrerUid: string = - 'uid=' + testLehrer.referrer + ',ou=ersatzSchulen,' + mockLdapInstanceConfig.BASE_DN; + const lehrerUid: string = 'uid=' + testLehrer.referrer + ',ou=ersatzSchulen,dc=schule-sh,dc=de'; const result: Result = await ldapClientService.createLehrer( testLehrer, fakeErsatzSchuleAddressDomain, - fakeOrgaKennung, undefined, ); @@ -832,8 +355,7 @@ describe('LDAP Client Service', () => { }); it('when lehrer already exists', async () => { - const lehrerUid: string = - 'uid=' + person.referrer + ',ou=oeffentlicheSchulen,' + mockLdapInstanceConfig.BASE_DN; + const lehrerUid: string = 'uid=' + person.referrer + ',ou=oeffentlicheSchulen,dc=schule-sh,dc=de'; ldapClientMock.getClient.mockImplementation(() => { clientMock.bind.mockResolvedValue(); clientMock.add.mockResolvedValueOnce(); @@ -849,11 +371,7 @@ describe('LDAP Client Service', () => { return clientMock; }); - const result: Result = await ldapClientService.createLehrer( - person, - fakeEmailDomain, - fakeOrgaKennung, - ); + const result: Result = await ldapClientService.createLehrer(person, fakeEmailDomain); expect(loggerMock.info).toHaveBeenLastCalledWith(`LDAP: Lehrer ${lehrerUid} exists, nothing to create`); expect(result.ok).toBeTruthy(); @@ -870,7 +388,6 @@ describe('LDAP Client Service', () => { const result: Result = await ldapClientService.createLehrer( personWithoutReferrer, fakeEmailDomain, - fakeOrgaKennung, ); expect(result.ok).toBeFalsy(); @@ -882,11 +399,7 @@ describe('LDAP Client Service', () => { clientMock.add.mockResolvedValueOnce(); return clientMock; }); - const result: Result = await ldapClientService.createLehrer( - person, - fakeEmailDomain, - fakeOrgaKennung, - ); + const result: Result = await ldapClientService.createLehrer(person, fakeEmailDomain); expect(result.ok).toBeFalsy(); }); @@ -895,121 +408,30 @@ describe('LDAP Client Service', () => { const result: Result = await ldapClientService.createLehrer( person, 'wrong-email-domain.de', - fakeOrgaKennung, ); if (result.ok) throw Error(); expect(result.error).toBeInstanceOf(LdapEmailDomainError); }); - - it('should log an error and return the failed result if addPersonToGroup fails', async () => { - const referrer: string = 'test-user'; - const schulId: string = '123'; - const expectedGroupId: string = `lehrer-${schulId}`; - const errorMessage: string = `LDAP: Failed to add lehrer ${referrer} to group ${expectedGroupId}`; - - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce(createMock({ searchEntries: [] })); - clientMock.add.mockRejectedValueOnce(new Error('Group addition failed')); - return clientMock; - }); - - jest.spyOn(ldapClientService, 'addPersonToGroup').mockResolvedValue({ - ok: false, - error: new Error('Group addition failed'), - }); - - const result: Result = await ldapClientService.createLehrer( - { - id: faker.string.uuid(), - vorname: faker.person.firstName(), - familienname: faker.person.lastName(), - referrer, - }, - 'schule-sh.de', - schulId, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw new Error('Test failed because result was unexpectedly successful'); - expect(loggerMock.error).toHaveBeenCalledWith(errorMessage); - expect(result.error?.message).toContain('Group addition failed'); - }); }); }); describe('deletion', () => { const fakeEmailDomain: string = 'schule-sh.de'; - const fakeOrgaKennung: string = '123'; describe('delete lehrer', () => { it('should return truthy result', async () => { ldapClientMock.getClient.mockImplementation(() => { clientMock.bind.mockResolvedValueOnce(); clientMock.del.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce(createMock()); - clientMock.search.mockResolvedValueOnce( - createMock({ - searchEntries: [createMock()], - }), - ); - return clientMock; - }); - - const result: Result = await ldapClientService.deleteLehrer( - person, - fakeOrgaKennung, - fakeEmailDomain, - ); - - expect(result.ok).toBeTruthy(); - }); - - it('should return truthy result, when person to delete is not found', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.del.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce(createMock()); - clientMock.search.mockResolvedValueOnce( - createMock({ - searchEntries: [], - }), - ); return clientMock; }); - const result: Result = await ldapClientService.deleteLehrer( - person, - fakeOrgaKennung, - fakeEmailDomain, - ); + const result: Result = await ldapClientService.deleteLehrer(person, fakeEmailDomain); expect(result.ok).toBeTruthy(); }); - it('should return error when deletion fails', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.del.mockRejectedValueOnce(new Error()); - clientMock.search.mockResolvedValueOnce(createMock()); - clientMock.search.mockResolvedValueOnce( - createMock({ - searchEntries: [createMock()], - }), - ); - return clientMock; - }); - - const result: Result = await ldapClientService.deleteLehrer( - person, - fakeOrgaKennung, - fakeEmailDomain, - ); - - expect(result.ok).toBeFalsy(); - }); - it('when called with person without referrer should return error result', async () => { ldapClientMock.getClient.mockImplementation(() => { clientMock.bind.mockResolvedValueOnce(); @@ -1018,7 +440,6 @@ describe('LDAP Client Service', () => { }); const result: Result = await ldapClientService.deleteLehrer( personWithoutReferrer, - fakeOrgaKennung, fakeEmailDomain, ); @@ -1031,11 +452,7 @@ describe('LDAP Client Service', () => { clientMock.add.mockResolvedValueOnce(); return clientMock; }); - const result: Result = await ldapClientService.deleteLehrer( - person, - fakeOrgaKennung, - fakeEmailDomain, - ); + const result: Result = await ldapClientService.deleteLehrer(person, fakeEmailDomain); expect(result.ok).toBeFalsy(); }); @@ -1043,7 +460,6 @@ describe('LDAP Client Service', () => { it('when called with invalid emailDomain returns LdapEmailDomainError', async () => { const result: Result = await ldapClientService.deleteLehrer( person, - fakeOrgaKennung, 'wrong-email-domain.de', ); @@ -1209,30 +625,6 @@ describe('LDAP Client Service', () => { expect(clientMock.modify).not.toHaveBeenCalled(); expect(clientMock.modifyDN).not.toHaveBeenCalled(); }); - - it('should return error if updateMemberDnInGroups fails', async () => { - const oldReferrer: string = faker.internet.userName(); - const newUid: string = faker.string.alphanumeric(6); - - jest.spyOn(ldapClientService, 'updateMemberDnInGroups').mockResolvedValueOnce({ - ok: false, - error: new Error('Failed to update groups'), - }); - - const result: Result = await ldapClientService.modifyPersonAttributes( - oldReferrer, - undefined, - undefined, - newUid, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error?.message).toBe('Failed to update groups'); - expect(loggerMock.error).toHaveBeenCalledWith( - `LDAP: Failed to update groups for person: ${oldReferrer}`, - ); - }); }); }); }); @@ -1330,8 +722,8 @@ describe('LDAP Client Service', () => { it('should set mailAlternativeAddress as current mailPrimaryAddress and throw LdapPersonEntryChangedEvent', async () => { ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce( + clientMock.bind.mockResolvedValue(); + clientMock.search.mockResolvedValue( createMock({ searchEntries: [ createMock({ @@ -1341,7 +733,7 @@ describe('LDAP Client Service', () => { ], }), ); - clientMock.modify.mockRejectedValueOnce(new Error()); + clientMock.modify.mockRejectedValue(new Error()); return clientMock; }); @@ -1354,7 +746,7 @@ describe('LDAP Client Service', () => { if (result.ok) throw Error(); expect(result.error).toStrictEqual(new LdapModifyEmailError()); - expect(loggerMock.error).toHaveBeenLastCalledWith( + expect(loggerMock.error).toHaveBeenCalledWith( `LDAP: Modifying mailPrimaryAddress and mailAlternativeAddress FAILED, errMsg:{}`, ); expect(eventServiceMock.publish).toHaveBeenCalledTimes(0); @@ -1502,280 +894,6 @@ describe('LDAP Client Service', () => { }); }); - describe('removePersonFromGroup', () => { - const fakeGroupId: string = 'lehrer-123'; - const fakePersonUid: string = 'user123'; - const fakeGroupDn: string = `cn=${fakeGroupId},${mockLdapInstanceConfig.BASE_DN}`; - const fakeLehrerUid: string = `uid=${fakePersonUid},ou=users,${mockLdapInstanceConfig.BASE_DN}`; - const fakeDienstStellenNummer: string = '123'; - - it('should successfully remove person from group with multiple members', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce( - createMock({ - searchEntries: [ - createMock({ - dn: fakeGroupDn, - member: [ - `${fakeLehrerUid}`, - 'uid=otherUser,ou=users,' + mockLdapInstanceConfig.BASE_DN, - ], - }), - ], - }), - ); - clientMock.modify.mockResolvedValueOnce(); - - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeTruthy(); - expect(clientMock.modify).toHaveBeenCalledWith(fakeGroupDn, [ - new Change({ - operation: 'delete', - modification: new Attribute({ - type: 'member', - values: [fakeLehrerUid], - }), - }), - ]); - expect(loggerMock.info).toHaveBeenCalledWith( - `LDAP: Successfully removed person ${fakePersonUid} from group ${fakeGroupId}`, - ); - }); - - it('should delete the group when only one member is present', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce( - createMock({ - searchEntries: [ - createMock({ - dn: fakeGroupDn, - member: `${fakeLehrerUid}`, - }), - ], - }), - ); - clientMock.del.mockResolvedValueOnce(); - - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeTruthy(); - expect(clientMock.del).toHaveBeenCalledWith(fakeGroupDn); - expect(loggerMock.info).toHaveBeenCalledWith( - `LDAP: Successfully removed person ${fakePersonUid} from group ${fakeGroupId}`, - ); - expect(loggerMock.info).toHaveBeenCalledWith(`LDAP: Successfully deleted group ${fakeGroupId}`); - }); - - it('should return error when group is not found', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce( - createMock({ - searchEntries: [], - }), - ); - - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error).toBeInstanceOf(Error); - }); - - it('should return error when bind fails', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockRejectedValueOnce(new Error()); - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error).toBeInstanceOf(Error); - }); - - it('should return error when modification fails', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce( - createMock({ - searchEntries: [ - createMock({ - dn: fakeGroupDn, - member: [ - `${fakeLehrerUid}`, - 'uid=otherUser,ou=users,' + mockLdapInstanceConfig.BASE_DN, - ], - }), - ], - }), - ); - clientMock.modify.mockRejectedValueOnce(new Error('Modify error')); - - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error).toBeInstanceOf(LdapRemovePersonFromGroupError); - expect(loggerMock.error).toHaveBeenCalledWith( - `LDAP: Failed to remove person from group ${fakeGroupId}, errMsg: Error: Modify error`, - ); - }); - - it('should return false when person is not in group (member as string)', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce({ - searchEntries: [ - { - dn: fakeGroupDn, - member: `uid=other-user,ou=users,${mockLdapInstanceConfig.BASE_DN}`, - }, - ], - searchReferences: [], - }); - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error).toBeInstanceOf(Error); - expect(result.error?.message).toContain(`Person ${fakePersonUid} is not in group ${fakeGroupId}`); - }); - - it('should return true when person is in group (member as Buffer)', async () => { - const bufferMember: Buffer = Buffer.from(`uid=${fakePersonUid},ou=users,${mockLdapInstanceConfig.BASE_DN}`); - - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce({ - searchEntries: [ - { - dn: fakeGroupDn, - member: bufferMember, - }, - ], - searchReferences: [], - }); - clientMock.del.mockResolvedValueOnce(); - - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeTruthy(); - if (!result.ok) throw Error(); - expect(loggerMock.info).toHaveBeenCalledWith( - `LDAP: Successfully removed person ${fakePersonUid} from group ${fakeGroupId}`, - ); - }); - - it('should return undefined when searchEntries is empty', async () => { - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce({ - searchEntries: [], // Leere Suchergebnisse - searchReferences: [], - }); - - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeFalsy(); - if (result.ok) throw Error(); - expect(result.error).toBeInstanceOf(Error); - expect(result.error?.message).toContain(`Group ${fakeGroupId} not found`); - }); - - it('should return true when person is in group (member as Buffer array)', async () => { - const bufferMemberArray: Buffer[] = [ - Buffer.from(`uid=${fakePersonUid},ou=users,${mockLdapInstanceConfig.BASE_DN}`), - Buffer.from(`uid=other-user,ou=users,${mockLdapInstanceConfig.BASE_DN}`), - ]; - - ldapClientMock.getClient.mockImplementation(() => { - clientMock.bind.mockResolvedValueOnce(); - clientMock.search.mockResolvedValueOnce({ - searchEntries: [ - { - dn: fakeGroupDn, - member: bufferMemberArray, - }, - ], - searchReferences: [], - }); - clientMock.modify.mockResolvedValueOnce(); - - return clientMock; - }); - - const result: Result = await ldapClientService.removePersonFromGroup( - fakePersonUid, - fakeDienstStellenNummer, - fakeLehrerUid, - ); - - expect(result.ok).toBeTruthy(); - if (!result.ok) throw Error(); - expect(loggerMock.info).toHaveBeenCalledWith( - `LDAP: Successfully removed person ${fakePersonUid} from group ${fakeGroupId}`, - ); - }); - }); - describe('changeUserPasswordByPersonId', () => { describe('when bind returns error', () => { it('should return falsy result', async () => { @@ -1894,32 +1012,4 @@ describe('LDAP Client Service', () => { }); }); }); - describe('createNewLehrerUidFromOldUid', () => { - it('should replace the old uid with the new referrer and join the DN parts with commas', () => { - const oldUid: string = 'uid=oldUser,ou=users,dc=example,dc=com'; - const newReferrer: string = 'newUser'; - - const result: string = ldapClientService.createNewLehrerUidFromOldUid(oldUid, newReferrer); - - expect(result).toBe('uid=newUser,ou=users,dc=example,dc=com'); - }); - - it('should handle a DN with only a uid component', () => { - const oldUid: string = 'uid=oldUser'; - const newReferrer: string = 'newUser'; - - const result: string = ldapClientService.createNewLehrerUidFromOldUid(oldUid, newReferrer); - - expect(result).toBe('uid=newUser'); - }); - - it('should handle an empty DN string', () => { - const oldUid: string = ''; - const newReferrer: string = 'newUser'; - - const result: string = ldapClientService.createNewLehrerUidFromOldUid(oldUid, newReferrer); - - expect(result).toBe('uid=newUser'); - }); - }); }); diff --git a/src/core/ldap/domain/ldap-client.service.ts b/src/core/ldap/domain/ldap-client.service.ts index b62460540..bc2984ead 100644 --- a/src/core/ldap/domain/ldap-client.service.ts +++ b/src/core/ldap/domain/ldap-client.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ClassLogger } from '../../logging/class-logger.js'; -import { Attribute, Change, Client, Control, Entry, SearchResult } from 'ldapts'; +import { Attribute, Change, Client, Control, SearchResult } from 'ldapts'; import { LdapEntityType, LdapPersonEntry } from './ldap.types.js'; import { LdapClient } from './ldap-client.js'; import { LdapInstanceConfig } from '../ldap-instance-config.js'; @@ -16,8 +16,6 @@ import { LdapCreateLehrerError } from '../error/ldap-create-lehrer.error.js'; import { LdapModifyEmailError } from '../error/ldap-modify-email.error.js'; import { LdapModifyUserPasswordError } from '../error/ldap-modify-user-password.error.js'; import { generatePassword } from '../../../shared/util/password-generator.js'; -import { LdapAddPersonToGroupError } from '../error/ldap-add-person-to-group.error.js'; -import { LdapRemovePersonFromGroupError } from '../error/ldap-remove-person-from-group.error.js'; export type PersonData = { vorname: string; @@ -29,6 +27,8 @@ export type PersonData = { @Injectable() export class LdapClientService { + public static readonly DEFAULT_RETRIES: number = 3; // e.g. DEFAULT_RETRIES = 3 will produce retry sequence: 1sek, 8sek, 27sek (1000ms * retrycounter^3) + public static readonly OEFFENTLICHE_SCHULEN_DOMAIN_DEFAULT: string = 'schule-sh.de'; public static readonly ERSATZ_SCHULEN_DOMAIN_DEFAULT: string = 'ersatzschule-sh.de'; @@ -53,8 +53,6 @@ export class LdapClientService { private static readonly RELAX_OID: string = '1.3.6.1.4.1.4203.666.5.12'; // Relax Control - private static readonly GROUPS: string = 'groups'; - private mutex: Mutex; public constructor( @@ -66,6 +64,68 @@ export class LdapClientService { this.mutex = new Mutex(); } + //** BELOW ONLY PUBLIC FUNCTIONS - MUST USE THE 'executeWithRetry' WRAPPER TO HAVE STRONG FAULT TOLERANCE*/ + + public async createLehrer(person: PersonData, domain: string, mail?: string): Promise> { + return this.executeWithRetry( + () => this.createLehrerInternal(person, domain, mail), + LdapClientService.DEFAULT_RETRIES, + ); + } + + public async changeEmailAddressByPersonId( + personId: PersonID, + referrer: PersonReferrer, + newEmailAddress: string, + ): Promise> { + return this.executeWithRetry( + () => this.changeEmailAddressByPersonIdInternal(personId, referrer, newEmailAddress), + LdapClientService.DEFAULT_RETRIES, + ); + } + + public async modifyPersonAttributes( + oldReferrer: string, + newGivenName?: string, + newSn?: string, + newUid?: string, + ): Promise> { + return this.executeWithRetry( + () => this.modifyPersonAttributesInternal(oldReferrer, newGivenName, newSn, newUid), + LdapClientService.DEFAULT_RETRIES, + ); + } + + public async deleteLehrerByReferrer(referrer: string): Promise> { + return this.executeWithRetry( + () => this.deleteLehrerByReferrerInternal(referrer), + LdapClientService.DEFAULT_RETRIES, + ); + } + + public async deleteLehrer(person: PersonData, domain: string): Promise> { + return this.executeWithRetry( + () => this.deleteLehrerInternal(person, domain), + LdapClientService.DEFAULT_RETRIES, + ); + } + + public async changeUserPasswordByPersonId(personId: PersonID, referrer: PersonReferrer): Promise> { + return this.executeWithRetry( + () => this.changeUserPasswordByPersonIdInternal(personId, referrer), + LdapClientService.DEFAULT_RETRIES, + ); + } + + public async isLehrerExisting(referrer: string, domain: string): Promise> { + return this.executeWithRetry( + () => this.isLehrerExistingInternal(referrer, domain), + LdapClientService.DEFAULT_RETRIES, + ); + } + + //** BELOW ONLY PRIVATE FUNCTIONS */ + private async bind(): Promise> { this.logger.info('LDAP: bind'); try { @@ -108,8 +168,8 @@ export class LdapClientService { }; } - public getLehrerUid(referrer: string, rootName: string): string { - return `uid=${referrer},ou=${rootName},${this.ldapInstanceConfig.BASE_DN}`; + private getLehrerUid(referrer: string, rootName: string): string { + return `uid=${referrer},ou=${rootName},${LdapClientService.DC_SCHULE_SH_DC_DE}`; } private getRootNameOrError(domain: string): Result { @@ -120,108 +180,83 @@ export class LdapClientService { return rootName; } - public async createLehrer( - person: PersonData, - domain: string, - schulId: string, - mail?: string, //Wird hier erstmal seperat mit reingegeben bis die Umstellung auf primary/alternative erfolgt - ): Promise> { - const referrer: string | undefined = person.referrer; - if (!referrer) { - return { - ok: false, - error: new UsernameRequiredError( - `Lehrer ${person.vorname} ${person.familienname} does not have a username`, - ), - }; - } + private async isLehrerExistingInternal(referrer: string, domain: string): Promise> { const rootName: Result = this.getRootNameOrError(domain); if (!rootName.ok) return rootName; - const lehrerUid: string = this.getLehrerUid(referrer, rootName.value); return this.mutex.runExclusive(async () => { - this.logger.info('LDAP: createLehrer'); + this.logger.info('LDAP: isLehrerExisting'); const client: Client = this.ldapClient.getClient(); const bindResult: Result = await this.bind(); if (!bindResult.ok) return bindResult; - const groupResult: Result = await this.addPersonToGroup(referrer, schulId, lehrerUid); - if (!groupResult.ok) { - this.logger.error(`LDAP: Failed to add lehrer ${referrer} to group lehrer-${schulId}`); - return groupResult; - } - const searchResultLehrer: SearchResult = await client.search( - `ou=${rootName.value},${this.ldapInstanceConfig.BASE_DN}`, + `ou=${rootName.value},${LdapClientService.DC_SCHULE_SH_DC_DE}`, { - filter: `(uid=${person.referrer})`, + filter: `(uid=${referrer})`, }, ); if (searchResultLehrer.searchEntries.length > 0) { - this.logger.info(`LDAP: Lehrer ${lehrerUid} exists, nothing to create`); - - return { ok: true, value: person }; - } - const entry: LdapPersonEntry = { - uid: referrer, - uidNumber: LdapClientService.UID_NUMBER, - gidNumber: LdapClientService.GID_NUMBER, - homeDirectory: LdapClientService.HOME_DIRECTORY, - cn: referrer, - givenName: person.vorname, - sn: person.familienname, - objectclass: ['inetOrgPerson', 'univentionMail', 'posixAccount'], - mailPrimaryAddress: mail ?? ``, - mailAlternativeAddress: mail ?? ``, - }; - - const controls: Control[] = []; - if (person.ldapEntryUUID) { - entry.entryUUID = person.ldapEntryUUID; - controls.push(new Control(LdapClientService.RELAX_OID)); + return { ok: true, value: true }; } + return { ok: true, value: false }; + }); + } - try { - await client.add(lehrerUid, entry, controls); - this.logger.info(`LDAP: Successfully created lehrer ${lehrerUid}`); + private async deleteLehrerByReferrerInternal(referrer: string): Promise> { + return this.mutex.runExclusive(async () => { + this.logger.info('LDAP: deleteLehrer'); + const client: Client = this.ldapClient.getClient(); + const bindResult: Result = await this.bind(); + if (!bindResult.ok) return bindResult; - return { ok: true, value: person }; - } catch (err) { - const errMsg: string = JSON.stringify(err); - this.logger.error(`LDAP: Creating lehrer FAILED, uid:${lehrerUid}, errMsg:${errMsg}`); - return { ok: false, error: new LdapCreateLehrerError() }; + const searchResultLehrer: SearchResult = await client.search(`${LdapClientService.DC_SCHULE_SH_DC_DE}`, { + scope: 'sub', + filter: `(uid=${referrer})`, + }); + if (!searchResultLehrer.searchEntries[0]) { + return { + ok: false, + error: new LdapSearchError(LdapEntityType.LEHRER), + }; } + await client.del(searchResultLehrer.searchEntries[0].dn); + this.logger.info(`LDAP: Successfully deleted lehrer by person:${referrer}`); + + return { ok: true, value: referrer }; }); } - public async isLehrerExisting(referrer: string, domain: string): Promise> { + private async deleteLehrerInternal(person: PersonData, domain: string): Promise> { const rootName: Result = this.getRootNameOrError(domain); if (!rootName.ok) return rootName; return this.mutex.runExclusive(async () => { - this.logger.info('LDAP: isLehrerExisting'); + this.logger.info('LDAP: deleteLehrer'); const client: Client = this.ldapClient.getClient(); const bindResult: Result = await this.bind(); if (!bindResult.ok) return bindResult; - - const searchResultLehrer: SearchResult = await client.search( - `ou=${rootName.value},${this.ldapInstanceConfig.BASE_DN}`, - { - filter: `(uid=${referrer})`, - }, - ); - if (searchResultLehrer.searchEntries.length > 0) { - return { ok: true, value: true }; + if (!person.referrer) { + return { + ok: false, + error: new UsernameRequiredError( + `Lehrer ${person.vorname} ${person.familienname} does not have a username`, + ), + }; } - return { ok: true, value: false }; + const lehrerUid: string = this.getLehrerUid(person.referrer, rootName.value); + await client.del(lehrerUid); + this.logger.info(`LDAP: Successfully deleted lehrer ${lehrerUid}`); + + return { ok: true, value: person }; }); } - public async modifyPersonAttributes( + private async modifyPersonAttributesInternal( oldReferrer: string, newGivenName?: string, newSn?: string, - newReferrer?: string, + newUid?: string, ): Promise> { return this.mutex.runExclusive(async () => { this.logger.info('LDAP: modifyPersonAttributes'); @@ -229,10 +264,10 @@ export class LdapClientService { const bindResult: Result = await this.bind(); if (!bindResult.ok) return bindResult; - const searchResult: SearchResult = await client.search(`${this.ldapInstanceConfig.BASE_DN}`, { + const searchResult: SearchResult = await client.search(`${LdapClientService.DC_SCHULE_SH_DC_DE}`, { scope: 'sub', filter: `(uid=${oldReferrer})`, - attributes: ['givenName', 'sn', 'uid', 'dn'], + attributes: ['givenName', 'sn', 'uid'], returnAttributeValues: true, }); if (!searchResult.searchEntries[0]) { @@ -246,13 +281,13 @@ export class LdapClientService { const entryDn: string = searchResult.searchEntries[0].dn; const modifications: Change[] = []; - if (newReferrer) { + if (newUid) { modifications.push( new Change({ operation: 'replace', modification: new Attribute({ type: 'cn', - values: [newReferrer], + values: [newUid], }), }), ); @@ -286,185 +321,24 @@ export class LdapClientService { this.logger.info(`No givenName/sn attributes provided to modify for person:${oldReferrer}`); } - if (newReferrer && searchResult.searchEntries[0]['uid'] !== newReferrer) { - const newDn: string = `uid=${newReferrer}`; + if (newUid && searchResult.searchEntries[0]['uid'] !== newUid) { + const newDn: string = `uid=${newUid}`; await client.modifyDN(entryDn, newDn); - this.logger.info(`LDAP: Successfully updated uid for person:${oldReferrer} to ${newReferrer}`); - } - - if (newReferrer) { - const groupUpdateResult: Result = await this.updateMemberDnInGroups( - oldReferrer, - newReferrer, - entryDn, - client, - ); - if (!groupUpdateResult.ok) { - this.logger.error(`LDAP: Failed to update groups for person: ${oldReferrer}`); - return groupUpdateResult; - } + this.logger.info(`LDAP: Successfully updated uid for person:${oldReferrer} to ${newUid}`); } return { ok: true, value: oldReferrer }; }); } - public createNewLehrerUidFromOldUid(oldUid: string, newReferrer: string): string { - const splitted: string[] = oldUid.split(','); - splitted[0] = `uid=${newReferrer}`; - return splitted.join(','); - } - - public async updateMemberDnInGroups( - oldReferrer: string, - newReferrer: string, - oldUid: string, - client: Client, - ): Promise> { - const oldLehrerUid: string = oldUid; - const newLehrerUid: string = this.createNewLehrerUidFromOldUid(oldUid, newReferrer); - - const searchResult: SearchResult = await client.search(`${this.ldapInstanceConfig.BASE_DN}`, { - scope: 'sub', - filter: `(member=${oldLehrerUid})`, - attributes: ['dn', 'member'], - returnAttributeValues: true, - }); - - const groupEntries: Entry[] | undefined = searchResult.searchEntries; - - if (!groupEntries) { - const errMsg: string = `LDAP: Error while searching for groups for person: ${oldReferrer}`; - this.logger.error(errMsg); - return { ok: false, error: new Error(errMsg) }; - } - - if (groupEntries.length === 0) { - this.logger.info(`LDAP: No groups found for person:${oldReferrer}`); - return { ok: true, value: `No groups found for person:${oldReferrer}` }; - } - - await Promise.allSettled( - groupEntries.map(async (entry: Entry) => { - const groupDn: string = entry.dn; - const members: string | string[] | Buffer | Buffer[] | undefined = entry['member']; - let existingMembers: string[] = []; - - if (Array.isArray(members)) { - existingMembers = members.map((member: string | Buffer) => { - if (Buffer.isBuffer(member)) { - return member.toString('utf-8'); - } else { - return member; - } - }); - } else if (typeof members === 'string') { - existingMembers = [members]; - } else if (Buffer.isBuffer(members)) { - existingMembers = [members.toString('utf-8')]; - } else { - existingMembers = []; - } - - const updatedMembers: (string | Buffer)[] = existingMembers.map((member: string | Buffer) => - member === oldLehrerUid ? newLehrerUid : member, - ); - - await client - .modify(groupDn, [ - new Change({ - operation: 'replace', - modification: new Attribute({ - type: 'member', - values: updatedMembers.map((member: string | Buffer) => member.toString()), - }), - }), - ]) - .catch((err: Error) => { - const errMsg: string = `LDAP: Error while updating member data for group: ${groupDn}, errMsg: ${String(err)}`; - this.logger.error(errMsg); - return { ok: false, error: new Error(errMsg) }; - }); - this.logger.info(`LDAP: Updated member data for group: ${groupDn}`); - }), - ); - return { ok: true, value: `Updated member data for ${groupEntries.length} groups.` }; - } - - public async deleteLehrerByReferrer(referrer: string): Promise> { - return this.mutex.runExclusive(async () => { - this.logger.info('LDAP: deleteLehrer by referrer'); - const client: Client = this.ldapClient.getClient(); - const bindResult: Result = await this.bind(); - if (!bindResult.ok) return bindResult; - - const searchResultLehrer: SearchResult = await client.search(`${this.ldapInstanceConfig.BASE_DN}`, { - scope: 'sub', - filter: `(uid=${referrer})`, - }); - if (!searchResultLehrer.searchEntries[0]) { - return { - ok: false, - error: new LdapSearchError(LdapEntityType.LEHRER), - }; - } - await client.del(searchResultLehrer.searchEntries[0].dn); - this.logger.info(`LDAP: Successfully deleted lehrer by referrer:${referrer}`); - - return { ok: true, value: referrer }; - }); - } - - public async deleteLehrer(person: PersonData, orgaKennung: string, domain: string): Promise> { - const rootName: Result = this.getRootNameOrError(domain); - if (!rootName.ok) return rootName; - - return this.mutex.runExclusive(async () => { - this.logger.info('LDAP: deleteLehrer by person'); - const client: Client = this.ldapClient.getClient(); - const bindResult: Result = await this.bind(); - if (!bindResult.ok) return bindResult; - if (!person.referrer) { - return { - ok: false, - error: new UsernameRequiredError( - `Lehrer ${person.vorname} ${person.familienname} does not have a username`, - ), - }; - } - const lehrerUid: string = this.getLehrerUid(person.referrer, rootName.value); - await this.removePersonFromGroup(person.referrer, orgaKennung, lehrerUid); - try { - const searchResultLehrer: SearchResult = await client.search( - `ou=${rootName.value},${this.ldapInstanceConfig.BASE_DN}`, - { - filter: `(uid=${person.referrer})`, - }, - ); - if (!searchResultLehrer.searchEntries[0]) { - this.logger.info(`LDAP: Lehrer ${lehrerUid} does not exist, nothing to delete`); - - return { ok: true, value: person }; - } - await client.del(lehrerUid); - this.logger.info(`LDAP: Successfully deleted lehrer ${lehrerUid}`); - - return { ok: true, value: person }; - } catch (err) { - const errMsg: string = JSON.stringify(err); - this.logger.error(`LDAP: Deleting lehrer FAILED, uid:${lehrerUid}, errMsg:${errMsg}`); - return { ok: false, error: new LdapCreateLehrerError() }; - } - }); - } - - public async changeEmailAddressByPersonId( + public async changeEmailAddressByPersonIdInternal( personId: PersonID, referrer: PersonReferrer, newEmailAddress: string, ): Promise> { // Converted to avoid PersonRepository-ref, UEM-password-generation //const referrer: string | undefined = await this.getPersonReferrerOrUndefined(personId); + return this.mutex.runExclusive(async () => { this.logger.info('LDAP: changeEmailAddress'); const splitted: string[] = newEmailAddress.split('@'); @@ -483,7 +357,7 @@ export class LdapClientService { const bindResult: Result = await this.bind(); if (!bindResult.ok) return bindResult; const searchResult: SearchResult = await client.search( - `ou=${rootName.value},${this.ldapInstanceConfig.BASE_DN}`, + `ou=${rootName.value},${LdapClientService.DC_SCHULE_SH_DC_DE}`, { scope: 'sub', filter: `(uid=${referrer})`, @@ -548,176 +422,86 @@ export class LdapClientService { }); } - public async addPersonToGroup( - personUid: string, - schoolReferrer: string, - lehrerUid: string, - ): Promise> { - const groupId: string = 'lehrer-' + schoolReferrer; - this.logger.info(`LDAP: Adding person ${personUid} to group ${groupId}`); - const client: Client = this.ldapClient.getClient(); - const bindResult: Result = await this.bind(); - if (!bindResult.ok) return bindResult; - - const orgUnitDn: string = `ou=${schoolReferrer},${this.ldapInstanceConfig.BASE_DN}`; - const searchResultOrgUnit: SearchResult = await client.search(`${this.ldapInstanceConfig.BASE_DN}`, { - filter: `(ou=${schoolReferrer})`, - }); - - if (!searchResultOrgUnit.searchEntries[0]) { - this.logger.info(`LDAP: organizationalUnit ${schoolReferrer} not found, creating organizationalUnit`); - - const newOrgUnit: { ou: string; objectClass: string } = { - ou: schoolReferrer, - objectClass: 'organizationalUnit', - }; - await client.add(orgUnitDn, newOrgUnit); - } - - const orgRoleDn: string = `cn=${LdapClientService.GROUPS},${orgUnitDn}`; - const searchResultOrgRole: SearchResult = await client.search(orgUnitDn, { - filter: `(cn=${LdapClientService.GROUPS})`, - }); - if (!searchResultOrgRole.searchEntries[0]) { - const newOrgRole: { cn: string; objectClass: string } = { - cn: LdapClientService.GROUPS, - objectClass: 'organizationalRole', - }; - await client.add(orgRoleDn, newOrgRole); - } - - const lehrerDn: string = `cn=${groupId},${orgRoleDn}`; - const searchResultGroupOfNames: SearchResult = await client.search(orgRoleDn, { - filter: `(cn=${groupId})`, - }); - if (!searchResultGroupOfNames.searchEntries[0]) { - const newLehrerGroup: { cn: string; objectclass: string[]; member: string[] } = { - cn: groupId, - objectclass: ['groupOfNames'], - member: [lehrerUid], + private async createLehrerInternal( + person: PersonData, + domain: string, + mail?: string, //Wird hier erstmal seperat mit reingegeben bis die Umstellung auf primary/alternative erfolgt + ): Promise> { + const referrer: string | undefined = person.referrer; + if (!referrer) { + return { + ok: false, + error: new UsernameRequiredError( + `Lehrer ${person.vorname} ${person.familienname} does not have a username`, + ), }; - try { - await client.add(lehrerDn, newLehrerGroup); - this.logger.info(`LDAP: Successfully created group ${groupId} and added person ${personUid}`); - return { ok: true, value: true }; - } catch (err) { - const errMsg: string = `LDAP: Failed to create group ${groupId}, errMsg: ${String(err)}`; - this.logger.error(errMsg); - return { ok: false, error: new LdapAddPersonToGroupError() }; - } - } - - if (this.isPersonInSearchResult(searchResultGroupOfNames.searchEntries[0], lehrerUid)) { - this.logger.info(`LDAP: Person ${personUid} is already in group ${groupId}`); - return { ok: true, value: false }; } + const rootName: Result = this.getRootNameOrError(domain); + if (!rootName.ok) return rootName; - try { - await client.modify(lehrerDn, [ - new Change({ - operation: 'add', - modification: new Attribute({ - type: 'member', - values: [lehrerUid], - }), - }), - ]); - this.logger.info(`LDAP: Successfully added person ${personUid} to group ${groupId}`); - return { ok: true, value: true }; - } catch (err) { - const errMsg: string = `LDAP: Failed to add person to group ${groupId}, errMsg: ${String(err)}`; - this.logger.error(errMsg); - return { ok: false, error: new LdapAddPersonToGroupError() }; - } - } - - public async removePersonFromGroup( - referrer: string, - schoolReferrer: string, - lehrerUid: string, - ): Promise> { - const groupId: string = 'lehrer-' + schoolReferrer; - this.logger.info(`LDAP: Removing person ${referrer} from group ${groupId}`); - const client: Client = this.ldapClient.getClient(); - const bindResult: Result = await this.bind(); - if (!bindResult.ok) return bindResult; - const searchResultOrgUnit: SearchResult = await client.search( - `cn=${LdapClientService.GROUPS},ou=${schoolReferrer},${this.ldapInstanceConfig.BASE_DN}`, - { - filter: `(cn=${groupId})`, - }, - ); + const lehrerUid: string = this.getLehrerUid(referrer, rootName.value); + return this.mutex.runExclusive(async () => { + this.logger.info('LDAP: createLehrer'); + const client: Client = this.ldapClient.getClient(); + const bindResult: Result = await this.bind(); + if (!bindResult.ok) return bindResult; - if (!searchResultOrgUnit.searchEntries[0]) { - const errMsg: string = `LDAP: Group ${groupId} not found`; - this.logger.error(errMsg); - return { ok: false, error: new Error(errMsg) }; - } + const searchResultLehrer: SearchResult = await client.search( + `ou=${rootName.value},${LdapClientService.DC_SCHULE_SH_DC_DE}`, + { + filter: `(uid=${person.referrer})`, + }, + ); + if (searchResultLehrer.searchEntries.length > 0) { + this.logger.info(`LDAP: Lehrer ${lehrerUid} exists, nothing to create`); - if (!this.isPersonInSearchResult(searchResultOrgUnit.searchEntries[0], lehrerUid)) { - this.logger.info(`LDAP: Person ${referrer} is not in group ${groupId}`); - return { ok: false, error: new Error(`Person ${referrer} is not in group ${groupId}`) }; - } - const groupDn: string = searchResultOrgUnit.searchEntries[0].dn; - try { - if (typeof searchResultOrgUnit.searchEntries[0]['member'] === 'string') { - await client.del(groupDn); - this.logger.info(`LDAP: Successfully removed person ${referrer} from group ${groupId}`); - this.logger.info(`LDAP: Successfully deleted group ${groupId}`); - return { ok: true, value: true }; + return { ok: true, value: person }; } - await client.modify(groupDn, [ - new Change({ - operation: 'delete', - modification: new Attribute({ - type: 'member', - values: [lehrerUid], - }), - }), - ]); - this.logger.info(`LDAP: Successfully removed person ${referrer} from group ${groupId}`); - return { ok: true, value: true }; - } catch (err) { - const errMsg: string = `LDAP: Failed to remove person from group ${groupId}, errMsg: ${String(err)}`; - this.logger.error(errMsg); - return { ok: false, error: new LdapRemovePersonFromGroupError() }; - } - } - - private isPersonInSearchResult(searchEntry: Entry, lehrerUid: string): boolean | undefined { - const member: string | string[] | Buffer | Buffer[] | undefined = searchEntry['member']; + const entry: LdapPersonEntry = { + uid: referrer, + uidNumber: LdapClientService.UID_NUMBER, + gidNumber: LdapClientService.GID_NUMBER, + homeDirectory: LdapClientService.HOME_DIRECTORY, + cn: referrer, + givenName: person.vorname, + sn: person.familienname, + objectclass: ['inetOrgPerson', 'univentionMail', 'posixAccount'], + mailPrimaryAddress: mail ?? ``, + mailAlternativeAddress: mail ?? ``, + }; - if (typeof member === 'string') { - return member === lehrerUid; - } + const controls: Control[] = []; + if (person.ldapEntryUUID) { + entry.entryUUID = person.ldapEntryUUID; + controls.push(new Control(LdapClientService.RELAX_OID)); + } - if (Buffer.isBuffer(member)) { - return member.toString() === lehrerUid; - } + try { + await client.add(lehrerUid, entry, controls); + this.logger.info(`LDAP: Successfully created lehrer ${lehrerUid}`); - if (Array.isArray(member)) { - return member.some((entry: string | Buffer) => { - if (typeof entry === 'string') { - return entry === lehrerUid; - } - return entry.toString() === lehrerUid; - }); - } + return { ok: true, value: person }; + } catch (err) { + const errMsg: string = JSON.stringify(err); + this.logger.error(`LDAP: Creating lehrer FAILED, uid:${lehrerUid}, errMsg:${errMsg}`); - return false; + return { ok: false, error: new LdapCreateLehrerError() }; + } + }); } - public async changeUserPasswordByPersonId(personId: PersonID, referrer: PersonReferrer): Promise> { + private async changeUserPasswordByPersonIdInternal( + personId: PersonID, + referrer: PersonReferrer, + ): Promise> { // Converted to avoid PersonRepository-ref, UEM-password-generation //const referrer: string | undefined = await this.getPersonReferrerOrUndefined(personId); const userPassword: string = generatePassword(); - return this.mutex.runExclusive(async () => { this.logger.info('LDAP: changeUserPassword'); const client: Client = this.ldapClient.getClient(); const bindResult: Result = await this.bind(); if (!bindResult.ok) return bindResult; - const searchResult: SearchResult = await client.search(`${LdapClientService.DC_SCHULE_SH_DC_DE}`, { scope: 'sub', filter: `(uid=${referrer})`, @@ -731,7 +515,6 @@ export class LdapClientService { error: new LdapSearchError(LdapEntityType.LEHRER), }; } - try { await client.modify(searchResult.searchEntries[0].dn, [ new Change({ @@ -744,14 +527,51 @@ export class LdapClientService { ]); this.logger.info(`LDAP: Successfully modified userPassword (UEM) for personId:${personId}`); this.eventService.publish(new LdapPersonEntryChangedEvent(personId, undefined, undefined, true)); - return { ok: true, value: userPassword }; } catch (err) { const errMsg: string = JSON.stringify(err); this.logger.error(`LDAP: Modifying userPassword (UEM) FAILED, errMsg:${errMsg}`); - return { ok: false, error: new LdapModifyUserPasswordError() }; } }); } + + private async executeWithRetry( + func: () => Promise>, + retries: number, + delay: number = 1000, + ): Promise> { + let currentAttempt: number = 1; + let result: Result = { + ok: false, + error: new Error('executeWithRetry default fallback'), + }; + + while (currentAttempt <= retries) { + try { + // eslint-disable-next-line no-await-in-loop + result = await func(); + if (result.ok) { + return result; + } else { + throw new Error(`Function returned error: ${result.error.message}`); + } + } catch (error) { + const currentDelay: number = delay * Math.pow(currentAttempt, 3); + this.logger.warning( + `Attempt ${currentAttempt} failed. Retrying in ${currentDelay}ms... Remaining retries: ${retries - currentAttempt}`, + ); + + // eslint-disable-next-line no-await-in-loop + await this.sleep(currentDelay); + } + currentAttempt++; + } + this.logger.error(`All ${retries} attempts failed. Exiting with failure.`); + return result; + } + + private async sleep(ms: number): Promise { + return new Promise((resolve: () => void) => setTimeout(resolve, ms)); + } }