From 000d5347de402bb1b2bfd27039d710c155daf5e5 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Fri, 22 May 2026 09:27:27 +0200 Subject: [PATCH 1/8] feat(oneidentity): add VISIT_UPDATED flow for access sync --- src/index.ts | 2 +- src/models/Event.ts | 1 + .../OneIdentityIntegrationQueueConsumer.ts | 1 + ...osalAndMembersToOneIdentityHandler.spec.ts | 1 + .../syncVisitToOneIdentityHandler.spec.ts | 329 +++++++++++++++++- .../syncVisitToOneIdentityHandler.ts | 138 +++++++- .../oneidentity/utils/ESSOneIdentity.ts | 15 + .../oneidentity/utils/OneIdentityApi.ts | 12 + .../utils/interfaces/VisitMessage.ts | 1 + 9 files changed, 477 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index 23409f92..17c2fb64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,7 +71,7 @@ async function bootstrap() { logger.logException('Unhandled NODE exception', error); }); - logger.logInfo(`Running connector service at localhost:${PORT}`, {}); + logger.logInfo(`Running connector services at localhost:${PORT}`, {}); if ( enableScicatProposalUpsert || diff --git a/src/models/Event.ts b/src/models/Event.ts index 2ed1cff6..add33a9b 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -4,4 +4,5 @@ export enum Event { PROPOSAL_UPDATED = 'PROPOSAL_UPDATED', VISIT_CREATED = 'VISIT_CREATED', VISIT_DELETED = 'VISIT_DELETED', + VISIT_UPDATED = 'VISIT_UPDATED', } diff --git a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts index 0dfaacbb..65d3d618 100644 --- a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts +++ b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts @@ -25,6 +25,7 @@ const SYNC_PROPOSAL_AND_MEMBERS_EVENTS_FOR_HANDLING = [ const SYNC_VISIT_EVENTS_FOR_HANDLING = [ Event.VISIT_CREATED, Event.VISIT_DELETED, + Event.VISIT_UPDATED, ]; // Class for consuming messages from the ESS One Identity Integration Queue diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts index cb8cf144..c0a08c6c 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts @@ -25,6 +25,7 @@ const mockOneIdentity: jest.Mocked> = { removeConnectionBetweenPersonAndProposal: jest.fn(), getPersonWantsOrg: jest.fn(), createPersonWantsOrg: jest.fn(), + updatePersonWantsOrg: jest.fn(), cancelPersonWantsOrg: jest.fn(), hasPersonSiteAccessToProposal: jest.fn(), }; diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts index ce60cc3c..6e5e5c33 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts @@ -38,6 +38,7 @@ const mockOneIdentity: jest.Mocked> = { getProposalPersonConnections: jest.fn(), removeConnectionBetweenPersonAndProposal: jest.fn(), createPersonWantsOrg: jest.fn(), + updatePersonWantsOrg: jest.fn(), cancelPersonWantsOrg: jest.fn(), hasPersonSiteAccessToProposal: jest.fn(), }; @@ -45,6 +46,7 @@ const mockOneIdentity: jest.Mocked> = { const mockUidESet: UID_ESet = 'eset-uid-123'; const visitMessage: VisitMessage = { + id: '1', visitorId: 'visitor-oidc-sub', startAt: '2023-01-01T00:00:00.000Z', endAt: '2023-01-10T00:00:00.000Z', @@ -144,7 +146,7 @@ describe('syncVisitToOneIdentityHandler', () => { visitMessage.visitorId, visitMessage.startAt, visitMessage.endAt, - visitMessage.proposal.shortCode + visitMessage.id ); // Calculate expected system access dates @@ -163,7 +165,7 @@ describe('syncVisitToOneIdentityHandler', () => { visitMessage.visitorId, expectedValidFrom, expectedEndDate.toISOString(), - 'site-access-uid' + visitMessage.id ); expect(logger.logInfo).toHaveBeenCalledWith( @@ -325,7 +327,7 @@ describe('syncVisitToOneIdentityHandler', () => { DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, ValidFrom: '2023-01-01T00:00:00.000Z', ValidUntil: '2023-01-10T00:00:00.000Z', - CustomProperty04: 'proposal-short-code', + CustomProperty04: visitMessageVisitorNotMember.id, OrderState: OrderState.GRANTED, } as PersonWantsOrg, { @@ -334,7 +336,7 @@ describe('syncVisitToOneIdentityHandler', () => { DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, ValidFrom: '2023-01-01T00:00:00.000Z', ValidUntil: '2023-01-10T00:00:00.000Z', - CustomProperty04: 'site-access-uid', + CustomProperty04: visitMessageVisitorNotMember.id, OrderState: OrderState.GRANTED, } as PersonWantsOrg, ]; @@ -423,7 +425,7 @@ describe('syncVisitToOneIdentityHandler', () => { DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, ValidFrom: '2023-01-01T00:00:00.000Z', ValidUntil: '2023-01-10T00:00:00.000Z', - CustomProperty04: 'proposal-short-code', + CustomProperty04: visitMessage.id, OrderState: OrderState.GRANTED, } as PersonWantsOrg, { @@ -432,7 +434,7 @@ describe('syncVisitToOneIdentityHandler', () => { DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, ValidFrom: '2023-01-01T00:00:00.000Z', ValidUntil: '2023-01-10T00:00:00.000Z', - CustomProperty04: 'site-access-uid', + CustomProperty04: visitMessage.id, OrderState: OrderState.GRANTED, } as PersonWantsOrg, ]; @@ -574,17 +576,17 @@ describe('syncVisitToOneIdentityHandler', () => { DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, ValidFrom: visitMessage.startAt, ValidUntil: visitMessage.endAt, - CustomProperty04: 'proposal-short-code', + CustomProperty04: visitMessage.id, OrderState: OrderState.GRANTED, } as PersonWantsOrg, - // No system access with CustomProperty04 matching site-access-uid + // No system access with CustomProperty04 matching visitMessage.id { UID_PersonWantsOrg: 'system-access-uid', UID_PersonOrdered: 'visitor-uid', DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, ValidFrom: '2023-01-01T00:00:00.000Z', ValidUntil: '2023-01-10T00:00:00.000Z', - CustomProperty04: 'different-site-access-uid', + CustomProperty04: 'different-visit-id', OrderState: OrderState.GRANTED, } as PersonWantsOrg, ]; @@ -617,4 +619,313 @@ describe('syncVisitToOneIdentityHandler', () => { expect(mockOneIdentity.logout).toHaveBeenCalled(); }); }); + + describe('VISIT_UPDATED', () => { + it('should update site access and system access dates when visit dates have changed', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + const oldSiteAccess = { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: '1', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg; + + const oldSystemAccess = { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-25T00:00:00.000Z', + CustomProperty04: '1', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg; + + const mockPersonWantsOrgs = [oldSiteAccess, oldSystemAccess]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([]); // No existing connection + + // Updated visit message with new dates + const updatedVisitMessage: VisitMessage = { + ...visitMessage, + startAt: '2023-02-01T00:00:00.000Z', + endAt: '2023-02-15T00:00:00.000Z', + }; + + await syncVisitToOneIdentityHandler( + updatedVisitMessage, + Event.VISIT_UPDATED + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getPersonWantsOrg).toHaveBeenCalledWith( + 'visitor-uid' + ); + + // Verify site access update + expect(mockOneIdentity.updatePersonWantsOrg).toHaveBeenNthCalledWith( + 1, + 'site-access-uid', + '2023-02-01T00:00:00.000Z', + '2023-02-15T00:00:00.000Z' + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Site access updated in One Identity', + { + UID_PersonWantsOrg: 'site-access-uid', + } + ); + + // Verify system access update + expect(mockOneIdentity.updatePersonWantsOrg).toHaveBeenNthCalledWith( + 2, + 'system-access-uid', + '2023-02-01T00:00:00.000Z', + '2023-02-15T00:00:00.000Z' + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'System access updated in One Identity', + { + UID_PersonWantsOrg: 'system-access-uid', + } + ); + + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + mockUidESet, + 'visitor-uid' + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should skip update when visit dates have not changed', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + const mockSiteAccess = { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: visitMessage.startAt, + ValidUntil: visitMessage.endAt, + CustomProperty04: '1', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg; + + const mockSystemAccess = { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: visitMessage.startAt, + ValidUntil: visitMessage.endAt, + CustomProperty04: '1', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg; + + const mockPersonWantsOrgs = [mockSiteAccess, mockSystemAccess]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([]); // No existing connection + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_UPDATED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + + // Verify no update occurred + expect(mockOneIdentity.updatePersonWantsOrg).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visit dates unchanged, skipping access update in One Identity', + { + UID_PersonWantsOrg: 'site-access-uid', + visitId: visitMessage.id, + } + ); + + // But proposal connection should still be created as backup + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + mockUidESet, + 'visitor-uid' + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should create new access if site access not found', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + const mockSiteAccess = { + UID_PersonWantsOrg: 'site-access-uid', + } as PersonWantsOrg; + const mockSystemAccess = { + UID_PersonWantsOrg: 'system-access-uid', + } as PersonWantsOrg; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce([]); // No existing access + mockOneIdentity.createPersonWantsOrg + .mockResolvedValueOnce([mockSiteAccess]) + .mockResolvedValueOnce([mockSystemAccess]); + mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([]); // No existing connection + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_UPDATED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getPersonWantsOrg).toHaveBeenCalledWith( + 'visitor-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'Site access not found in One Identity, creating access', + { + visitId: visitMessage.id, + uidPerson: 'visitor-uid', + } + ); + + // Verify creation occurred instead of update + expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenCalledTimes(2); + expect(mockOneIdentity.updatePersonWantsOrg).not.toHaveBeenCalled(); + + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + mockUidESet, + 'visitor-uid' + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if system access not found during update', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + const mockSiteAccess = { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: '1', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg; + + const mockPersonWantsOrgs = [mockSiteAccess]; // Only site access, no system access + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + const updatedVisitMessage: VisitMessage = { + ...visitMessage, + startAt: '2023-02-01T00:00:00.000Z', + endAt: '2023-02-15T00:00:00.000Z', + }; + + await expect( + syncVisitToOneIdentityHandler(updatedVisitMessage, Event.VISIT_UPDATED) + ).rejects.toThrow( + 'System access not found in One Identity, cannot update access' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should skip processing if visitor is not a science user', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: 'EMPLOYEEDK', + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_UPDATED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visitor is not a Science User, skipping', + {} + ); + expect(mockOneIdentity.getPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.updatePersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if proposal is not found in One Identity', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(undefined); // Proposal not found + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_UPDATED) + ).rejects.toThrow( + 'Proposal not found in One Identity, cannot sync visit' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessage.proposal + ); + expect(mockOneIdentity.updatePersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if person not found', async () => { + mockOneIdentity.getPerson.mockResolvedValueOnce(undefined); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_UPDATED) + ).rejects.toThrow('Person not found in One Identity'); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); }); diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts index 781f1d7b..699a4c9d 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -18,7 +18,7 @@ const ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS = parseInt( ); export async function syncVisitToOneIdentityHandler( - { startAt, endAt, visitorId: oidcSub, proposal }: VisitMessage, + { id: visitId, startAt, endAt, visitorId: oidcSub, proposal }: VisitMessage, type: Event ): Promise { const oneIdentity = new ESSOneIdentity(); @@ -44,16 +44,29 @@ export async function syncVisitToOneIdentityHandler( if (type === Event.VISIT_CREATED) { await createAccessInOneIdentity( oneIdentity, + visitId, startAt, endAt, - oidcSub, - proposal + oidcSub ); // Every visitor should have access to the proposal folders await createProposalConnection(oneIdentity, uidESet, uidPerson); + } else if (type === Event.VISIT_UPDATED) { + // For simplicity, we will just update the access with the new dates. The update takes place only if the dates are changed. + await updateAccessInOneIdentity( + oneIdentity, + visitId, + startAt, + endAt, + uidPerson, + oidcSub + ); + + // If the Proposal connection has not been established during Visit Creation, this will be a backup + await createProposalConnection(oneIdentity, uidESet, uidPerson); } else if (type === Event.VISIT_DELETED) { - await removeAccessFromOneIdentity(oneIdentity, startAt, endAt, uidPerson); + await removeAccessFromOneIdentity(oneIdentity, visitId, uidPerson); // Remove the connection between the proposal and the visitor await removeProposalConnection( @@ -90,10 +103,10 @@ async function getScienceUser( async function createAccessInOneIdentity( oneIdentity: ESSOneIdentity, + visitId: string, startAt: string, endAt: string, - centralAccount: string, - proposal: ProposalMessageData + centralAccount: string ) { // Create site access const [pwoSite] = await oneIdentity.createPersonWantsOrg( @@ -101,7 +114,7 @@ async function createAccessInOneIdentity( centralAccount, toIsoString(startAt), toIsoString(endAt), - proposal.shortCode // CustomProperty04 - We store the proposal short code for the site access to be able to find it later + visitId.toString() // CustomProperty04 - We store the visit ID for the site access to be able to find it later ); logger.logInfo('Site access created in One Identity', { @@ -120,7 +133,7 @@ async function createAccessInOneIdentity( centralAccount, toIsoString(validFrom), toIsoString(validUntil), - pwoSite.UID_PersonWantsOrg // CustomProperty04 - We store the site access UID for the system access to be able to find it later + visitId.toString() // CustomProperty04 - We store the visit ID for the system access to be able to find it later ); logger.logInfo('System access created in One Identity', { @@ -128,10 +141,99 @@ async function createAccessInOneIdentity( }); } -async function removeAccessFromOneIdentity( +async function updateAccessInOneIdentity( oneIdentity: ESSOneIdentity, + visitId: string, startAt: string, endAt: string, + uidPerson: UID_Person, + centralAccount: string +) { + // Find person wants orgs for the visitor + const personWantsOrgs = await oneIdentity.getPersonWantsOrg(uidPerson); + + // Find site access for the visitor + const siteAccess = personWantsOrgs.find( + (pwo) => + pwo.DisplayOrg === PersonWantsOrgRole.SITE_ACCESS && + pwo.CustomProperty04 === visitId.toString() && // CustomProperty04 is the visit ID for the site access + pwo.OrderState !== OrderState.ABORTED + ); + + // If there is no existing siteAccess record, new one will be created for both siteAccess and systemAccess. + if (!siteAccess) { + logger.logInfo('Site access not found in One Identity, creating access', { + visitId, + uidPerson, + }); + + await createAccessInOneIdentity( + oneIdentity, + visitId, + startAt, + endAt, + centralAccount + ); + + return; + } + + const validFrom = toIsoString(startAt); + const validUntil = toIsoString(endAt); + + if ( + isSameDateTime(siteAccess.ValidFrom, validFrom) && + isSameDateTime(siteAccess.ValidUntil, validUntil) + ) { + logger.logInfo( + 'Visit dates unchanged, skipping access update in One Identity', + { + UID_PersonWantsOrg: siteAccess.UID_PersonWantsOrg, + visitId, + } + ); + + return; + } + + await oneIdentity.updatePersonWantsOrg( + siteAccess.UID_PersonWantsOrg, + validFrom, + validUntil + ); + + logger.logInfo('Site access updated in One Identity', { + UID_PersonWantsOrg: siteAccess.UID_PersonWantsOrg, + }); + + // Find system access for the site access (CustomProperty04 is the visit ID) + const systemAccess = personWantsOrgs.find( + (pwo) => + pwo.CustomProperty04 === visitId.toString() && + pwo.DisplayOrg === PersonWantsOrgRole.SYSTEM_ACCESS && + pwo.OrderState !== OrderState.UNSUBSCRIBED + ); + + if (!systemAccess) { + throw new Error( + 'System access not found in One Identity, cannot update access' + ); + } + + await oneIdentity.updatePersonWantsOrg( + systemAccess.UID_PersonWantsOrg, + validFrom, + validUntil + ); + + logger.logInfo('System access updated in One Identity', { + UID_PersonWantsOrg: systemAccess.UID_PersonWantsOrg, + }); +} + +async function removeAccessFromOneIdentity( + oneIdentity: ESSOneIdentity, + visitId: string, uidPerson: UID_Person ) { // Find person wants orgs for the visitor @@ -141,8 +243,7 @@ async function removeAccessFromOneIdentity( const siteAccess = personWantsOrgs.find( (pwo) => pwo.DisplayOrg === PersonWantsOrgRole.SITE_ACCESS && - toIsoString(pwo.ValidFrom) === toIsoString(startAt) && - toIsoString(pwo.ValidUntil) === toIsoString(endAt) && + pwo.CustomProperty04 === visitId.toString() && // CustomProperty04 is the visit ID for the site access pwo.OrderState !== OrderState.ABORTED ); @@ -158,10 +259,10 @@ async function removeAccessFromOneIdentity( UID_PersonWantsOrg: siteAccess.UID_PersonWantsOrg, }); - // Find system access for the site access (CustomProperty04 is the site access UID) + // Find system access for the site access (CustomProperty04 is the visit ID) const systemAccess = personWantsOrgs.find( (pwo) => - pwo.CustomProperty04 === siteAccess.UID_PersonWantsOrg && + pwo.CustomProperty04 === visitId.toString() && pwo.DisplayOrg === PersonWantsOrgRole.SYSTEM_ACCESS && pwo.OrderState !== OrderState.UNSUBSCRIBED ); @@ -244,3 +345,14 @@ function toIsoString(date: string | number) { return parsedDate.toISOString(); } + +function isSameDateTime(left: string, right: string) { + const leftTime = new Date(left).getTime(); + const rightTime = new Date(right).getTime(); + + if (isNaN(leftTime) || isNaN(rightTime)) { + return left === right; + } + + return leftTime === rightTime; +} diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts index ad79a121..17ab4cc5 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts @@ -166,6 +166,21 @@ export class ESSOneIdentity { return res.Data; } + public async updatePersonWantsOrg( + uidPersonWantsOrg: string, + startDate: string, + endDate: string + ): Promise { + await this.oneIdentityApi.updateEntity( + 'PersonWantsOrg', + uidPersonWantsOrg, + { + ValidFrom: startDate, + ValidUntil: endDate, + } + ); + } + public async cancelPersonWantsOrg(uidPersonWantsOrg: string): Promise { const res = await this.oneIdentityApi.callScript( diff --git a/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts b/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts index 20c2045b..90e63fb1 100644 --- a/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts +++ b/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts @@ -87,6 +87,18 @@ export class OneIdentityApi { return data; } + public async updateEntity( + table: string, + uid: string, + values: Partial + ): Promise { + const { data } = await this.axiosInstance.put(`/entity/${table}/${uid}`, { + values, + }); + + return data; + } + public async getEntities( table: string, where: string, diff --git a/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts b/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts index ce180475..d273cfad 100644 --- a/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts +++ b/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts @@ -1,6 +1,7 @@ import { ProposalMessageData } from '../../../../../models/ProposalMessage'; export interface VisitMessage { + id: string; startAt: string; endAt: string; visitorId: string; From 3a0fd9cc825c2c399087bbbc4b12ef152dc60b4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 07:49:29 +0000 Subject: [PATCH 2/8] fix: apply buffer days to system access ValidUntil on update (same as creation) Agent-Logs-Url: https://github.com/UserOfficeProject/connector/sessions/18fd40a2-d897-4c0f-a140-94c2adc14acd --- package-lock.json | 20 ++----------------- .../syncVisitToOneIdentityHandler.spec.ts | 11 ++++++++-- .../syncVisitToOneIdentityHandler.ts | 8 +++++++- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index f39edfdc..4365c8d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,7 +183,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -1674,7 +1673,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.16.1", @@ -2029,7 +2027,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2520,7 +2517,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001503", "electron-to-chromium": "^1.4.431", @@ -3487,7 +3483,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3543,7 +3538,6 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5171,7 +5165,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7277,7 +7270,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9077,7 +9069,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, - "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -10275,7 +10266,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", "dev": true, - "peer": true, "requires": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.16.1", @@ -10486,8 +10476,7 @@ "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "peer": true + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" }, "acorn-jsx": { "version": "5.3.2", @@ -10846,7 +10835,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, - "peer": true, "requires": { "caniuse-lite": "^1.0.30001503", "electron-to-chromium": "^1.4.431", @@ -11513,7 +11501,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -11572,7 +11559,6 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, - "peer": true, "requires": {} }, "eslint-import-resolver-node": { @@ -12690,7 +12676,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -14162,8 +14147,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true, - "peer": true + "dev": true }, "prettier-linter-helpers": { "version": "1.0.0", diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts index 6e5e5c33..38a0c8fc 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts @@ -690,12 +690,19 @@ describe('syncVisitToOneIdentityHandler', () => { } ); - // Verify system access update + // Verify system access update (ValidUntil extended by ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS) + const expectedSystemAccessValidUntil = new Date( + '2023-02-15T00:00:00.000Z' + ); + expectedSystemAccessValidUntil.setDate( + expectedSystemAccessValidUntil.getDate() + + parseInt(ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS) + ); expect(mockOneIdentity.updatePersonWantsOrg).toHaveBeenNthCalledWith( 2, 'system-access-uid', '2023-02-01T00:00:00.000Z', - '2023-02-15T00:00:00.000Z' + expectedSystemAccessValidUntil.toISOString() ); expect(logger.logInfo).toHaveBeenCalledWith( 'System access updated in One Identity', diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts index 699a4c9d..ebdbae4e 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -220,10 +220,16 @@ async function updateAccessInOneIdentity( ); } + const systemAccessValidUntil = toIsoString( + new Date(endAt).setDate( + new Date(endAt).getDate() + ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS + ) + ); + await oneIdentity.updatePersonWantsOrg( systemAccess.UID_PersonWantsOrg, validFrom, - validUntil + systemAccessValidUntil ); logger.logInfo('System access updated in One Identity', { From e71e4300f5c10cc1717607110e310a3fd1eea26c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 07:51:56 +0000 Subject: [PATCH 3/8] fix: update axios to 1.15.2 to address security vulnerabilities Agent-Logs-Url: https://github.com/UserOfficeProject/connector/sessions/18fd40a2-d897-4c0f-a140-94c2adc14acd --- package-lock.json | 67 +++++++++++++++++++++++++---------------------- package.json | 2 +- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4365c8d8..0a1d6966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@user-office-software/duo-logger": "^2.2.1", "@user-office-software/duo-message-broker": "^1.7.0", - "axios": "^1.12.0", + "axios": "^1.15.2", "dotenv": "^16.4.5", "envalid": "^8.0.0", "express": "^4.21.2", @@ -2316,14 +2316,14 @@ } }, "node_modules/axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "node_modules/babel-jest": { @@ -4192,15 +4192,16 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -4220,9 +4221,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -7356,9 +7357,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -10680,13 +10685,13 @@ } }, "axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "requires": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "babel-jest": { @@ -12031,9 +12036,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==" }, "for-each": { "version": "0.3.3", @@ -12045,9 +12050,9 @@ } }, "form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -14206,9 +14211,9 @@ } }, "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==" }, "punycode": { "version": "2.3.1", diff --git a/package.json b/package.json index f1291f4b..451a14e9 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@user-office-software/duo-logger": "^2.2.1", "@user-office-software/duo-message-broker": "^1.7.0", - "axios": "^1.12.0", + "axios": "^1.15.2", "dotenv": "^16.4.5", "envalid": "^8.0.0", "express": "^4.21.2", From c5285798531542288bf24eca066e4b6a3513cc94 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 07:57:21 +0000 Subject: [PATCH 4/8] fix: address review comments - fix updateEntity return type and revert log message Agent-Logs-Url: https://github.com/UserOfficeProject/connector/sessions/f8cb93b2-b131-43ba-990b-ee3d9d8f436a --- src/index.ts | 2 +- src/queue/consumers/oneidentity/utils/OneIdentityApi.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 17c2fb64..23409f92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,7 +71,7 @@ async function bootstrap() { logger.logException('Unhandled NODE exception', error); }); - logger.logInfo(`Running connector services at localhost:${PORT}`, {}); + logger.logInfo(`Running connector service at localhost:${PORT}`, {}); if ( enableScicatProposalUpsert || diff --git a/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts b/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts index 90e63fb1..3f50561f 100644 --- a/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts +++ b/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts @@ -92,11 +92,9 @@ export class OneIdentityApi { uid: string, values: Partial ): Promise { - const { data } = await this.axiosInstance.put(`/entity/${table}/${uid}`, { + await this.axiosInstance.put(`/entity/${table}/${uid}`, { values, }); - - return data; } public async getEntities( From 9a80d149c71f63fbda5c6568afc7f886e641cfb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 09:08:47 +0000 Subject: [PATCH 5/8] Validate system access exists before updating site access in One Identity Agent-Logs-Url: https://github.com/UserOfficeProject/connector/sessions/52158ce8-921e-46bd-a6bd-f72851eba47b --- .../syncVisitToOneIdentityHandler.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts index ce5ba86d..e2c862b0 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -181,6 +181,20 @@ async function updateAccessInOneIdentity( const validFrom = toIsoString(startAt); const validUntil = toIsoString(endAt); + // Find system access for the site access (CustomProperty04 is the visit ID) + const systemAccess = personWantsOrgs.find( + (pwo) => + pwo.CustomProperty04 === visitId.toString() && + pwo.DisplayOrg === PersonWantsOrgRole.SYSTEM_ACCESS && + pwo.OrderState !== OrderState.UNSUBSCRIBED + ); + + if (!systemAccess) { + throw new Error( + 'System access not found in One Identity, cannot update access' + ); + } + if ( isSameDateTime(siteAccess.ValidFrom, validFrom) && isSameDateTime(siteAccess.ValidUntil, validUntil) @@ -206,20 +220,6 @@ async function updateAccessInOneIdentity( UID_PersonWantsOrg: siteAccess.UID_PersonWantsOrg, }); - // Find system access for the site access (CustomProperty04 is the visit ID) - const systemAccess = personWantsOrgs.find( - (pwo) => - pwo.CustomProperty04 === visitId.toString() && - pwo.DisplayOrg === PersonWantsOrgRole.SYSTEM_ACCESS && - pwo.OrderState !== OrderState.UNSUBSCRIBED - ); - - if (!systemAccess) { - throw new Error( - 'System access not found in One Identity, cannot update access' - ); - } - const systemAccessValidUntil = toIsoString( new Date(endAt).setDate( new Date(endAt).getDate() + ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS From fa174e7779afd285590410942c43cf40f86f1640 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Tue, 26 May 2026 11:43:33 +0200 Subject: [PATCH 6/8] fix: remove unnecessary toString conversion for visitId in access handling functions --- .../syncVisitToOneIdentityHandler.ts | 12 ++++++------ .../oneidentity/utils/isVisitMessage.spec.ts | 13 +++++++++++++ .../consumers/oneidentity/utils/isVisitMessage.ts | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts index e2c862b0..cc363d54 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -114,7 +114,7 @@ async function createAccessInOneIdentity( centralAccount, toIsoString(startAt), toIsoString(endAt), - visitId.toString() // CustomProperty04 - We store the visit ID for the site access to be able to find it later + visitId // CustomProperty04 - We store the visit ID for the site access to be able to find it later ); logger.logInfo('Site access created in One Identity', { @@ -133,7 +133,7 @@ async function createAccessInOneIdentity( centralAccount, toIsoString(validFrom), toIsoString(validUntil), - visitId.toString() // CustomProperty04 - We store the visit ID for the system access to be able to find it later + visitId // CustomProperty04 - We store the visit ID for the system access to be able to find it later ); logger.logInfo('System access created in One Identity', { @@ -156,7 +156,7 @@ async function updateAccessInOneIdentity( const siteAccess = personWantsOrgs.find( (pwo) => pwo.DisplayOrg === PersonWantsOrgRole.SITE_ACCESS && - pwo.CustomProperty04 === visitId.toString() && // CustomProperty04 is the visit ID for the site access + pwo.CustomProperty04 === visitId && // CustomProperty04 is the visit ID for the site access pwo.OrderState !== OrderState.ABORTED ); @@ -184,7 +184,7 @@ async function updateAccessInOneIdentity( // Find system access for the site access (CustomProperty04 is the visit ID) const systemAccess = personWantsOrgs.find( (pwo) => - pwo.CustomProperty04 === visitId.toString() && + pwo.CustomProperty04 === visitId && pwo.DisplayOrg === PersonWantsOrgRole.SYSTEM_ACCESS && pwo.OrderState !== OrderState.UNSUBSCRIBED ); @@ -249,7 +249,7 @@ async function removeAccessFromOneIdentity( const siteAccess = personWantsOrgs.find( (pwo) => pwo.DisplayOrg === PersonWantsOrgRole.SITE_ACCESS && - pwo.CustomProperty04 === visitId.toString() && // CustomProperty04 is the visit ID for the site access + pwo.CustomProperty04 === visitId && // CustomProperty04 is the visit ID for the site access pwo.OrderState !== OrderState.ABORTED ); @@ -268,7 +268,7 @@ async function removeAccessFromOneIdentity( // Find system access for the site access (CustomProperty04 is the visit ID) const systemAccess = personWantsOrgs.find( (pwo) => - pwo.CustomProperty04 === visitId.toString() && + pwo.CustomProperty04 === visitId && pwo.DisplayOrg === PersonWantsOrgRole.SYSTEM_ACCESS && pwo.OrderState !== OrderState.UNSUBSCRIBED ); diff --git a/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts b/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts index 5f7c0d78..9c593de2 100644 --- a/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts +++ b/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts @@ -44,8 +44,21 @@ describe('isVisitMessage', () => { expect(isVisitMessage(message)).toBe(false); }); + it('should return false if id is undefined', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + proposal: { + shortCode: 'proposal-short-code', + }, + }; + expect(isVisitMessage(message)).toBe(false); + }); + it('should return true if the message is valid', () => { const message = { + id: 'visit123', visitorId: 'visitor123', startAt: '2023-01-01T00:00:00Z', endAt: '2023-01-02T00:00:00Z', diff --git a/src/queue/consumers/oneidentity/utils/isVisitMessage.ts b/src/queue/consumers/oneidentity/utils/isVisitMessage.ts index ed7cb282..167dcc1a 100644 --- a/src/queue/consumers/oneidentity/utils/isVisitMessage.ts +++ b/src/queue/consumers/oneidentity/utils/isVisitMessage.ts @@ -4,6 +4,7 @@ export function isVisitMessage(message: any): message is VisitMessage { return ( message != null && typeof message === 'object' && + 'id' in message && 'visitorId' in message && 'startAt' in message && 'endAt' in message && From e2b37581bcd27e8c302049932879cd990e64dc47 Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Tue, 26 May 2026 14:23:40 +0200 Subject: [PATCH 7/8] refactor: replace createPersonWantsOrg and updatePersonWantsOrg with upsertPersonWantsOrg in One Identity handlers --- ...osalAndMembersToOneIdentityHandler.spec.ts | 3 +- .../syncVisitToOneIdentityHandler.spec.ts | 48 ++++++++++--------- .../syncVisitToOneIdentityHandler.ts | 22 +++++---- .../oneidentity/utils/ESSOneIdentity.spec.ts | 6 +-- .../oneidentity/utils/ESSOneIdentity.ts | 22 ++------- 5 files changed, 47 insertions(+), 54 deletions(-) diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts index f23a67af..60d14aa8 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts @@ -25,8 +25,7 @@ const mockOneIdentity: jest.Mocked> = { getProposalPersonConnections: jest.fn(), removeConnectionBetweenPersonAndProposal: jest.fn(), getPersonWantsOrg: jest.fn(), - createPersonWantsOrg: jest.fn(), - updatePersonWantsOrg: jest.fn(), + upsertPersonWantsOrg: jest.fn(), cancelPersonWantsOrg: jest.fn(), hasPersonSiteAccessToProposal: jest.fn(), }; diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts index 7a1a41c5..15b3d0b1 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts @@ -37,8 +37,7 @@ const mockOneIdentity: jest.Mocked> = { connectPersonToProposal: jest.fn(), getProposalPersonConnections: jest.fn(), removeConnectionBetweenPersonAndProposal: jest.fn(), - createPersonWantsOrg: jest.fn(), - updatePersonWantsOrg: jest.fn(), + upsertPersonWantsOrg: jest.fn(), cancelPersonWantsOrg: jest.fn(), hasPersonSiteAccessToProposal: jest.fn(), }; @@ -107,7 +106,7 @@ describe('syncVisitToOneIdentityHandler', () => { 'Visitor is not a Science User, skipping', {} ); - expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.upsertPersonWantsOrg).not.toHaveBeenCalled(); expect(mockOneIdentity.logout).toHaveBeenCalled(); }); }); @@ -138,7 +137,7 @@ describe('syncVisitToOneIdentityHandler', () => { mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([]); // No existing connection // Mock sequential calls to createPersonWantsOrg with different responses - mockOneIdentity.createPersonWantsOrg + mockOneIdentity.upsertPersonWantsOrg .mockResolvedValueOnce([mockSiteAccess]) .mockResolvedValueOnce([mockSystemAccess]); @@ -156,8 +155,8 @@ describe('syncVisitToOneIdentityHandler', () => { ); // Verify site access creation - expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenCalledTimes(2); - expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenNthCalledWith( + expect(mockOneIdentity.upsertPersonWantsOrg).toHaveBeenCalledTimes(2); + expect(mockOneIdentity.upsertPersonWantsOrg).toHaveBeenNthCalledWith( 1, PersonWantsOrgRole.SITE_ACCESS, visitMessage.visitorId, @@ -176,7 +175,7 @@ describe('syncVisitToOneIdentityHandler', () => { ); // Verify system access creation - expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenNthCalledWith( + expect(mockOneIdentity.upsertPersonWantsOrg).toHaveBeenNthCalledWith( 2, PersonWantsOrgRole.SYSTEM_ACCESS, visitMessage.visitorId, @@ -233,7 +232,7 @@ describe('syncVisitToOneIdentityHandler', () => { mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([ { UID_Person: mockPerson.UID_Person, UID_ESet: mockUidESet }, ]); // Connection exists - mockOneIdentity.createPersonWantsOrg + mockOneIdentity.upsertPersonWantsOrg .mockResolvedValueOnce([mockSiteAccess]) .mockResolvedValueOnce([mockSystemAccess]); @@ -272,7 +271,7 @@ describe('syncVisitToOneIdentityHandler', () => { expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( visitMessage.proposal ); - expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.upsertPersonWantsOrg).not.toHaveBeenCalled(); expect(mockOneIdentity.logout).toHaveBeenCalled(); }); @@ -285,7 +284,7 @@ describe('syncVisitToOneIdentityHandler', () => { mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); - mockOneIdentity.createPersonWantsOrg.mockRejectedValueOnce( + mockOneIdentity.upsertPersonWantsOrg.mockRejectedValueOnce( new Error('Failed to create site access') ); @@ -324,7 +323,7 @@ describe('syncVisitToOneIdentityHandler', () => { expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( 'visitor-oidc-sub' ); - expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.upsertPersonWantsOrg).not.toHaveBeenCalled(); expect(mockOneIdentity.logout).toHaveBeenCalled(); }); }); @@ -744,11 +743,14 @@ describe('syncVisitToOneIdentityHandler', () => { ); // Verify site access update - expect(mockOneIdentity.updatePersonWantsOrg).toHaveBeenNthCalledWith( + expect(mockOneIdentity.upsertPersonWantsOrg).toHaveBeenNthCalledWith( 1, - 'site-access-uid', + PersonWantsOrgRole.SITE_ACCESS, + 'visitor-oidc-sub', '2023-02-01T00:00:00.000Z', - '2023-02-15T00:00:00.000Z' + '2023-02-15T00:00:00.000Z', + '1', + 'site-access-uid' ); expect(logger.logInfo).toHaveBeenCalledWith( 'Site access updated in One Identity', @@ -765,11 +767,14 @@ describe('syncVisitToOneIdentityHandler', () => { expectedSystemAccessValidUntil.getDate() + parseInt(ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS) ); - expect(mockOneIdentity.updatePersonWantsOrg).toHaveBeenNthCalledWith( + expect(mockOneIdentity.upsertPersonWantsOrg).toHaveBeenNthCalledWith( 2, - 'system-access-uid', + PersonWantsOrgRole.SYSTEM_ACCESS, + 'visitor-oidc-sub', '2023-02-01T00:00:00.000Z', - expectedSystemAccessValidUntil.toISOString() + expectedSystemAccessValidUntil.toISOString(), + '1', + 'system-access-uid' ); expect(logger.logInfo).toHaveBeenCalledWith( 'System access updated in One Identity', @@ -828,7 +833,7 @@ describe('syncVisitToOneIdentityHandler', () => { ); // Verify no update occurred - expect(mockOneIdentity.updatePersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.upsertPersonWantsOrg).toHaveBeenCalledTimes(0); expect(logger.logInfo).toHaveBeenCalledWith( 'Visit dates unchanged, skipping access update in One Identity', { @@ -861,7 +866,7 @@ describe('syncVisitToOneIdentityHandler', () => { mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce([]); // No existing access - mockOneIdentity.createPersonWantsOrg + mockOneIdentity.upsertPersonWantsOrg .mockResolvedValueOnce([mockSiteAccess]) .mockResolvedValueOnce([mockSystemAccess]); mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([]); // No existing connection @@ -885,8 +890,7 @@ describe('syncVisitToOneIdentityHandler', () => { ); // Verify creation occurred instead of update - expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenCalledTimes(2); - expect(mockOneIdentity.updatePersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.upsertPersonWantsOrg).toHaveBeenCalledTimes(2); expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( mockUidESet, @@ -957,7 +961,6 @@ describe('syncVisitToOneIdentityHandler', () => { {} ); expect(mockOneIdentity.getPersonWantsOrg).not.toHaveBeenCalled(); - expect(mockOneIdentity.updatePersonWantsOrg).not.toHaveBeenCalled(); expect(mockOneIdentity.logout).toHaveBeenCalled(); }); @@ -983,7 +986,6 @@ describe('syncVisitToOneIdentityHandler', () => { expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( visitMessage.proposal ); - expect(mockOneIdentity.updatePersonWantsOrg).not.toHaveBeenCalled(); expect(mockOneIdentity.logout).toHaveBeenCalled(); }); diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts index cc363d54..d0a2d382 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -109,7 +109,7 @@ async function createAccessInOneIdentity( centralAccount: string ) { // Create site access - const [pwoSite] = await oneIdentity.createPersonWantsOrg( + const [pwoSite] = await oneIdentity.upsertPersonWantsOrg( PersonWantsOrgRole.SITE_ACCESS, centralAccount, toIsoString(startAt), @@ -128,7 +128,7 @@ async function createAccessInOneIdentity( ); // Create system access - const [pwoSystem] = await oneIdentity.createPersonWantsOrg( + const [pwoSystem] = await oneIdentity.upsertPersonWantsOrg( PersonWantsOrgRole.SYSTEM_ACCESS, centralAccount, toIsoString(validFrom), @@ -210,10 +210,13 @@ async function updateAccessInOneIdentity( return; } - await oneIdentity.updatePersonWantsOrg( - siteAccess.UID_PersonWantsOrg, + await oneIdentity.upsertPersonWantsOrg( + PersonWantsOrgRole.SITE_ACCESS, + centralAccount, validFrom, - validUntil + validUntil, + visitId, + siteAccess.UID_PersonWantsOrg ); logger.logInfo('Site access updated in One Identity', { @@ -226,10 +229,13 @@ async function updateAccessInOneIdentity( ) ); - await oneIdentity.updatePersonWantsOrg( - systemAccess.UID_PersonWantsOrg, + await oneIdentity.upsertPersonWantsOrg( + PersonWantsOrgRole.SYSTEM_ACCESS, + centralAccount, validFrom, - systemAccessValidUntil + systemAccessValidUntil, + visitId, + systemAccess.UID_PersonWantsOrg ); logger.logInfo('System access updated in One Identity', { diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts index 918e09f2..05fa8b71 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts @@ -310,7 +310,7 @@ describe('ESSOneIdentity', () => { Message: 'Success', }); - const result = await essOneIdentity.createPersonWantsOrg( + const result = await essOneIdentity.upsertPersonWantsOrg( role, centralAccount, startDate, @@ -332,7 +332,7 @@ describe('ESSOneIdentity', () => { Message: 'Success', }); - const result = await essOneIdentity.createPersonWantsOrg( + const result = await essOneIdentity.upsertPersonWantsOrg( role, centralAccount, startDate, @@ -364,7 +364,7 @@ describe('ESSOneIdentity', () => { }); await expect( - essOneIdentity.createPersonWantsOrg( + essOneIdentity.upsertPersonWantsOrg( role, centralAccount, startDate, diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts index 85ab0f5d..8b993f65 100644 --- a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts @@ -139,12 +139,13 @@ export class ESSOneIdentity { return entities.map(({ values }) => values); } - public async createPersonWantsOrg( + public async upsertPersonWantsOrg( role: PersonWantsOrgRole, centralAccount: string, startDate: string, endDate: string, - customData: string = '' + customData: string = '', + uidPersonWantsOrg: string = '' ): Promise { const res = await this.oneIdentityApi.callScript( @@ -156,7 +157,7 @@ export class ESSOneIdentity { startDate, endDate, customData, // PersonWantsOrg.CustomProperty04 - '', // UID_PersonWantsOrg (empty for new) + uidPersonWantsOrg, // UID_PersonWantsOrg (empty for new, provided for update) ] ); @@ -166,21 +167,6 @@ export class ESSOneIdentity { return res.Data; } - public async updatePersonWantsOrg( - uidPersonWantsOrg: string, - startDate: string, - endDate: string - ): Promise { - await this.oneIdentityApi.updateEntity( - 'PersonWantsOrg', - uidPersonWantsOrg, - { - ValidFrom: startDate, - ValidUntil: endDate, - } - ); - } - public async cancelPersonWantsOrg(uidPersonWantsOrg: string): Promise { const res = await this.oneIdentityApi.callScript( From 4263c658da5b506f9e8a45c9e57c9079a178e32f Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Tue, 26 May 2026 14:33:04 +0200 Subject: [PATCH 8/8] fix: mock Date.now for system access update and use it in upsertPersonWantsOrg --- .../consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts | 6 +++++- .../consumerCallbacks/syncVisitToOneIdentityHandler.ts | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts index 15b3d0b1..34219cd3 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts @@ -722,6 +722,10 @@ describe('syncVisitToOneIdentityHandler', () => { ); mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([]); // No existing connection + // Mock Date.now for system access update (same as creation logic) + const mockNowDate = new Date(); + Date.now = jest.fn(() => mockNowDate.getTime()); + // Updated visit message with new dates const updatedVisitMessage: VisitMessage = { ...visitMessage, @@ -771,7 +775,7 @@ describe('syncVisitToOneIdentityHandler', () => { 2, PersonWantsOrgRole.SYSTEM_ACCESS, 'visitor-oidc-sub', - '2023-02-01T00:00:00.000Z', + mockNowDate.toISOString(), expectedSystemAccessValidUntil.toISOString(), '1', 'system-access-uid' diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts index d0a2d382..8fb9599a 100644 --- a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -223,6 +223,7 @@ async function updateAccessInOneIdentity( UID_PersonWantsOrg: siteAccess.UID_PersonWantsOrg, }); + const systemAccessValidFrom = toIsoString(Date.now()); const systemAccessValidUntil = toIsoString( new Date(endAt).setDate( new Date(endAt).getDate() + ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS @@ -232,7 +233,7 @@ async function updateAccessInOneIdentity( await oneIdentity.upsertPersonWantsOrg( PersonWantsOrgRole.SYSTEM_ACCESS, centralAccount, - validFrom, + systemAccessValidFrom, systemAccessValidUntil, visitId, systemAccess.UID_PersonWantsOrg