diff --git a/.github/workflows/e2e-keycloak.yaml b/.github/workflows/e2e-keycloak.yaml index 2e767d376..152d59273 100644 --- a/.github/workflows/e2e-keycloak.yaml +++ b/.github/workflows/e2e-keycloak.yaml @@ -35,14 +35,25 @@ jobs: summary-title: 'Pre-reqs' wait-on: https://bcgov.github.io/sso-requests-sandbox/ wait-on-timeout: 120 - record: true install-command: yarn working-directory: app spec: | cypress/e2e/**/integration-990-deleteAllIntegrations.cy.ts browser: electron - # project: ./e2e - ci-build-id: ${{ github.event.number }} + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: app/cypress/screenshots/**/* + if-no-files-found: ignore + + # - uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: cypress-videos + # path: app/cypress/videos/**/* + # if-no-files-found: ignore idp-stopper: runs-on: ubuntu-latest @@ -60,7 +71,6 @@ jobs: continue-on-error: false with: summary-title: 'E2E tests' - record: true wait-on: 'https://bcgov.github.io/sso-requests-sandbox/' wait-on-timeout: 360 install-command: yarn @@ -68,7 +78,20 @@ jobs: spec: | cypress/e2e/external/idpstopper-*.cy.ts browser: electron - ci-build-id: ${{ github.event.number }} + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: app/cypress/screenshots/**/* + if-no-files-found: ignore + + # - uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: cypress-videos + # path: app/cypress/videos/**/* + # if-no-files-found: ignore search-users: runs-on: ubuntu-latest @@ -83,7 +106,6 @@ jobs: continue-on-error: false with: summary-title: 'E2E tests' - record: true wait-on: 'https://bcgov.github.io/sso-requests-sandbox/' wait-on-timeout: 360 install-command: yarn @@ -91,7 +113,20 @@ jobs: spec: | cypress/e2e/external/search-users.cy.ts browser: electron - ci-build-id: ${{ github.event.number }} + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: app/cypress/screenshots/**/* + if-no-files-found: ignore + + # - uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: cypress-videos + # path: app/cypress/videos/**/* + # if-no-files-found: ignore roles-tests: runs-on: ubuntu-latest @@ -106,7 +141,6 @@ jobs: continue-on-error: false with: summary-title: 'E2E tests' - record: true wait-on: 'https://bcgov.github.io/sso-requests-sandbox/' wait-on-timeout: 360 install-command: yarn @@ -114,7 +148,20 @@ jobs: spec: | cypress/e2e/external/integration-roles.cy.ts browser: electron - ci-build-id: ${{ github.event.number }} + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: app/cypress/screenshots/**/* + if-no-files-found: ignore + + # - uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: cypress-videos + # path: app/cypress/videos/**/* + # if-no-files-found: ignore integration-tests: runs-on: ubuntu-latest @@ -129,7 +176,6 @@ jobs: continue-on-error: false with: summary-title: 'E2E tests' - record: true wait-on: 'https://bcgov.github.io/sso-requests-sandbox/' wait-on-timeout: 360 install-command: yarn @@ -137,4 +183,17 @@ jobs: spec: | cypress/e2e/ci/integrations-crud.cy.ts browser: electron - ci-build-id: ${{ github.event.number }} + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: app/cypress/screenshots/**/* + if-no-files-found: ignore + + # - uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: cypress-videos + # path: app/cypress/videos/**/* + # if-no-files-found: ignore diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 0d6de0ba1..4ff512b8d 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -16,7 +16,6 @@ env: CYPRESS_guid: ${{ secrets.CYPRESS_GUID }} CYPRESS_loginproxy: ${{ secrets.CYPRESS_LOGINPROXY }} CYPRESS_siteminder: ${{ secrets.CYPRESS_SITEMINDER }} - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CYPRESS_host: 'http://localhost:3000' CYPRESS_smoketest: true @@ -39,14 +38,25 @@ jobs: continue-on-error: false with: summary-title: 'E2E tests' - record: true wait-on: 'http://localhost:3000' - parallel: true wait-on-timeout: 360 install-command: yarn working-directory: app spec: | - cypress/e2e/smoke/smoke-10-brokenlinks.cy.ts + cypress/e2e/ci/smoke-10-brokenlinks.cy.ts cypress/e2e/ci/*.cy.ts browser: electron - ci-build-id: ${{ github.event.number }} + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: app/cypress/screenshots/**/* + if-no-files-found: ignore + + # - uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: cypress-videos + # path: app/cypress/videos/**/* + # if-no-files-found: ignore diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 9e1997b33..d2de6b827 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -64,7 +64,8 @@ jobs: UPTIME_STATUS_DOMAIN_NAME=status.sandbox.loginproxy.gov.bc.ca AWS_ECR_URI=${{secrets.DEV_AWS_ECR_URI}} INCLUDE_DIGITAL_CREDENTIAL=true - INSTALL_SSO_CSS_GRAFANA=true + INSTALL_GRAFANA=true + INSTALL_REDIS=true INCLUDE_BC_SERVICES_CARD=true ALLOW_BC_SERVICES_CARD_PROD=true @@ -116,7 +117,8 @@ jobs: UPTIME_STATUS_DOMAIN_NAME=status.sandbox.loginproxy.gov.bc.ca AWS_ECR_URI=${{secrets.DEV_AWS_ECR_URI}} INCLUDE_DIGITAL_CREDENTIAL=true - INSTALL_SSO_CSS_GRAFANA=false + INSTALL_GRAFANA=false + INSTALL_REDIS=true INCLUDE_BC_SERVICES_CARD=true ALLOW_BC_SERVICES_CARD_PROD=false @@ -167,7 +169,8 @@ jobs: CUSTOM_DOMAIN_NAME=api.loginproxy.gov.bc.ca UPTIME_STATUS_DOMAIN_NAME=status.loginproxy.gov.bc.ca AWS_ECR_URI=${{secrets.PROD_AWS_ECR_URI}} - INSTALL_SSO_CSS_GRAFANA=true + INSTALL_GRAFANA=true + INSTALL_REDIS=true INCLUDE_DIGITAL_CREDENTIAL=true INCLUDE_BC_SERVICES_CARD=true @@ -309,7 +312,8 @@ jobs: ms_graph_api_authority="${{secrets.MS_GRAPH_API_AUTHORITY}}" ms_graph_api_client_id="${{secrets.MS_GRAPH_API_CLIENT_ID}}" ms_graph_api_client_secret="${{secrets.MS_GRAPH_API_CLIENT_SECRET}}" - install_sso_css_grafana="${{ env.INSTALL_SSO_CSS_GRAFANA == 'true' && 1 || 0 }}" + install_grafana="${{ env.INSTALL_GRAFANA == 'true' && 1 || 0 }}" + install_redis="${{ env.INSTALL_REDIS == 'true' && 1 || 0 }}" allow_bc_services_card_prod="${{env.ALLOW_BC_SERVICES_CARD_PROD}}" include_bc_services_card="${{env.INCLUDE_BC_SERVICES_CARD}}" @@ -319,7 +323,6 @@ jobs: bcsc_registration_base_url_dev="${{env.BCSC_REGISTRATION_BASE_URL_DEV}}" bcsc_registration_base_url_test="${{env.BCSC_REGISTRATION_BASE_URL_TEST}}" bcsc_registration_base_url_prod="${{env.BCSC_REGISTRATION_BASE_URL_PROD}}" - EOF working-directory: ./terraform diff --git a/.tool-versions b/.tool-versions index bd9cd9dd9..973e8d441 100644 --- a/.tool-versions +++ b/.tool-versions @@ -7,3 +7,4 @@ terraform-docs 0.12.1 tflint 0.41.0 k6 0.34.1 helm 3.10.2 +bun 1.1.34 diff --git a/Dockerfile b/Dockerfile index 42eebb256..5a42977a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ -FROM node:16.14.2-slim +FROM node:20.17.0-slim +RUN npm i -g bun RUN apt-get update && apt-get install curl make -y \ && apt-get install libsqlite3-dev bzip2 icu-devtools uuid-dev -y diff --git a/Dockerfile.tf-modules b/Dockerfile.tf-modules index 1e4933186..4b3ac9d94 100644 --- a/Dockerfile.tf-modules +++ b/Dockerfile.tf-modules @@ -1,5 +1,7 @@ -FROM docker.io/hashicorp/terraform:latest +FROM docker.io/hashicorp/terraform:1.1.4 +RUN apk update +RUN apk upgrade RUN apk add --no-cache curl # Set working directory diff --git a/app/cypress.config.ts b/app/cypress.config.ts index 34d1b8107..e64fa0bce 100644 --- a/app/cypress.config.ts +++ b/app/cypress.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ json: true, }, e2e: { - baseUrl: 'http://localhost:3000', + baseUrl: 'https://bcgov.github.io/sso-requests-sandbox', projectId: 'gctfmh', setupNodeEvents(on, config) { on('before:browser:launch', (browser, launchOptions) => { diff --git a/app/jest/ssoDashboard.test.tsx b/app/jest/ssoDashboard.test.tsx index 9b95815f7..d12a354c8 100644 --- a/app/jest/ssoDashboard.test.tsx +++ b/app/jest/ssoDashboard.test.tsx @@ -3,6 +3,8 @@ import AdminDashboard from 'pages/admin-dashboard'; import { Integration } from 'interfaces/Request'; import { sampleRequest } from './samples/integrations'; import { deleteRequest, updateRequestMetadata, updateRequest, restoreRequest, getRequestAll } from 'services/request'; +import BcServicesCardTabContent from 'page-partials/admin-dashboard/AdminTabs/BcServicesCardTabContent'; + import { getCompositeClientRoles } from '@app/services/keycloak'; import { debug } from 'jest-preview'; @@ -479,4 +481,31 @@ describe('SSO Dashboard', () => { expect(screen.queryByText('Remove Service Account')).not.toBeInTheDocument(); }); + + it('BCSC Tab can approve submitted bcsc integrations', () => { + const bcServicesCardIntegration: Integration = { + ...sampleRequest, + devIdps: ['bcservicescard'], + status: 'applied', + publicAccess: false, + }; + + render(); + const textElement = screen.getByText('To begin the BC Services Card integration in production, Click Below.'); + expect(textElement).toBeInTheDocument(); + }); + + it('BCSC Tab cannot approve archived bcsc integrations', () => { + const bcServicesCardIntegrationArchived: Integration = { + ...sampleRequest, + devIdps: ['bcservicescard'], + status: 'applied', + archived: true, + publicAccess: false, + }; + + render(); + const textElement = screen.getByText('Cannot approve deleted/archived integrations.'); + expect(textElement).toBeInTheDocument(); + }); }); diff --git a/app/page-partials/admin-dashboard/AdminTabs/TabContent.tsx b/app/page-partials/admin-dashboard/AdminTabs/TabContent.tsx index 0a5c759ef..b1efc28d1 100644 --- a/app/page-partials/admin-dashboard/AdminTabs/TabContent.tsx +++ b/app/page-partials/admin-dashboard/AdminTabs/TabContent.tsx @@ -86,7 +86,14 @@ function TabContent({ integration, type, canApproveProd, awaitingTFComplete, onA } let content; - if (canApproveProd) { + + if (integration?.archived) { + content = ( + <> +

Cannot approve deleted/archived integrations.

+ + ); + } else if (canApproveProd) { content = ( <>

{`To begin the ${displayType} integration in production, Click Below.`}

diff --git a/app/page-partials/my-dashboard/IntegrationInfoTabs/LogsPanel.tsx b/app/page-partials/my-dashboard/IntegrationInfoTabs/LogsPanel.tsx index 9e90d46b8..0d35d184c 100644 --- a/app/page-partials/my-dashboard/IntegrationInfoTabs/LogsPanel.tsx +++ b/app/page-partials/my-dashboard/IntegrationInfoTabs/LogsPanel.tsx @@ -99,11 +99,10 @@ interface Props { * @param filename Name to give to the downloaded file * @param dataObjToWrite The data to include in the file */ -const saveTemplateAsFile = (filename: string, dataObjToWrite: any) => { - const blob = new Blob([JSON.stringify(dataObjToWrite)], { type: 'text/json' }); +const saveTemplateAsFile = (filename: string, dataObjToWrite: Blob) => { const link = document.createElement('a'); link.download = filename; - link.href = window.URL.createObjectURL(blob); + link.href = window.URL.createObjectURL(dataObjToWrite); link.dataset.downloadurl = ['text/json', link.download, link.href].join(':'); const evt = new MouseEvent('click', { @@ -174,6 +173,8 @@ const LogsPanel = ({ integration, alert }: Props) => { }; const handleFileProgress = (progressEvent: AxiosProgressEvent) => { + // Ignore progress indicator if unsupported in browser. Some browsers ignore content-length for zipped responses. + if (progressEvent.total === undefined || progressEvent.loaded === undefined) return; const percentComplete = Math.floor((progressEvent.loaded / Number(progressEvent.total)) * 100); if (percentComplete !== fileProgress) { setFileProgress(percentComplete); @@ -222,11 +223,13 @@ const LogsPanel = ({ integration, alert }: Props) => { if (err) { // Ignore error if request cancelled if (err.code === 'ERR_CANCELED') return; + let content = 'Error fetching logs.'; + if (err?.status === 429) content = 'Too many requests'; alert.show({ variant: 'danger', fadeOut: 10000, closable: true, - content: err?.response?.data?.message ?? 'Error fetching logs.', + content, }); } else { alert.show({ @@ -235,10 +238,9 @@ const LogsPanel = ({ integration, alert }: Props) => { closable: true, content: result?.message ?? 'Downloaded logs.', }); - const resultJSON = await result.text(); saveTemplateAsFile( `${integration.clientId}-${fromDate.toLocaleString()}-${toDate.toLocaleString()}.json`, - JSON.parse(resultJSON), + result.data, ); surveyContext?.setShowSurvey(true, 'downloadLogs'); } diff --git a/app/page-partials/my-dashboard/TeamInfoTabs/ServiceAccountsList.tsx b/app/page-partials/my-dashboard/TeamInfoTabs/ServiceAccountsList.tsx index e25a9bb26..af1ca7f17 100644 --- a/app/page-partials/my-dashboard/TeamInfoTabs/ServiceAccountsList.tsx +++ b/app/page-partials/my-dashboard/TeamInfoTabs/ServiceAccountsList.tsx @@ -111,7 +111,7 @@ function ServiceAccountsList({ data = data || {}; const text = { - tokenUrl: `${data['auth-server-url']}/realms/${data.realm}/protocol/openid-connect/token`, + tokenUrl: `${data['token-url']}`, clientId: `${data.resource}`, clientSecret: `${data.credentials?.secret}`, }; diff --git a/app/services/grafana.ts b/app/services/grafana.ts index c79e33e51..1b5c414bc 100644 --- a/app/services/grafana.ts +++ b/app/services/grafana.ts @@ -1,4 +1,4 @@ -import { AxiosProgressEvent } from 'axios'; +import { AxiosError, AxiosProgressEvent } from 'axios'; import { instance } from './axios'; export const getMetrics = async (id: number, env: string, fromDate?: string, toDate?: string) => { @@ -21,18 +21,16 @@ export const getLogs = async ( toDate: Date, onProgress: (progressEvent: AxiosProgressEvent) => void, controller?: AbortController, -) => { +): Promise<[{ data: Blob; message: string }, null] | [null, AxiosError]> => { try { const result = await instance({ url: `requests/${id}/logs?env=${env}&start=${fromDate}&end=${toDate}`, responseType: 'blob', onDownloadProgress: onProgress, signal: controller?.signal, - }).then((res: any) => res?.data); - + }).then((res) => ({ data: res?.data, message: res?.headers['x-message'] })); return [result, null]; } catch (err) { - console.error(err); - return [null, err]; + return [null, err as AxiosError]; } }; diff --git a/docker-compose.yml b/docker-compose.yml index 7c2eb8532..53b73b8f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,8 @@ services: SSO_REQUESTS_BACKEND_HOSTNAME: sso-requests APP_URL: http://localhost:3000 INCLUDE_DIGITAL_CREDENTIAL: 'true' + INCLUDE_BC_SERVICES_CARD: 'true' + ALLOW_BC_SERVICES_CARD_PROD: 'true' depends_on: - sso-requests networks: @@ -91,6 +93,8 @@ services: BCSC_REGISTRATION_BASE_URL_DEV: https://idsit.gov.bc.ca BCSC_REGISTRATION_BASE_URL_TEST: https://idsit.gov.bc.ca BCSC_REGISTRATION_BASE_URL_PROD: https://idsit.gov.bc.ca + INCLUDE_BC_SERVICES_CARD: 'true' + ALLOW_BC_SERVICES_CARD_PROD: 'true' networks: css-net: aliases: [sso-requests-api] diff --git a/lambda/__tests__/01.auth.test.ts b/lambda/__tests__/01.auth.test.ts index a22214050..c12ab09d2 100644 --- a/lambda/__tests__/01.auth.test.ts +++ b/lambda/__tests__/01.auth.test.ts @@ -3,14 +3,6 @@ import { getAppApiHeartBeat } from './helpers/modules/common'; import { createIntegration, getIntegrations, getListOfIntegrations } from './helpers/modules/integrations'; import { TEAM_ADMIN_IDIR_EMAIL_01, TEAM_ADMIN_IDIR_USERID_01 } from './helpers/fixtures'; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('@lambda-app/controllers/requests', () => { const original = jest.requireActual('@lambda-app/controllers/requests'); return { diff --git a/lambda/__tests__/02.users-and-teams.test.ts b/lambda/__tests__/02.users-and-teams.test.ts index 71398896b..1fe2694a1 100644 --- a/lambda/__tests__/02.users-and-teams.test.ts +++ b/lambda/__tests__/02.users-and-teams.test.ts @@ -29,14 +29,6 @@ import { sendEmail } from '@lambda-shared/utils/ches'; import { models } from '@lambda-shared/sequelize/models/models'; import { findOrCreateUser } from '@lambda-app/controllers/user'; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('@lambda-app/controllers/requests', () => { const original = jest.requireActual('@lambda-app/controllers/requests'); return { diff --git a/lambda/__tests__/03.requests-by-users.test.ts b/lambda/__tests__/03.requests-by-users.test.ts index 91bda4eef..e3ac76e8b 100644 --- a/lambda/__tests__/03.requests-by-users.test.ts +++ b/lambda/__tests__/03.requests-by-users.test.ts @@ -50,14 +50,6 @@ const integrationRoles = [ }, ]; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/04.requests-by-teams.test.ts b/lambda/__tests__/04.requests-by-teams.test.ts index 1e75053df..5bb152e11 100644 --- a/lambda/__tests__/04.requests-by-teams.test.ts +++ b/lambda/__tests__/04.requests-by-teams.test.ts @@ -26,14 +26,6 @@ import { models } from '@lambda-shared/sequelize/models/models'; import { buildIntegration } from './helpers/modules/common'; import { Integration } from 'app/interfaces/Request'; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/05.requests-roles.test.ts b/lambda/__tests__/05.requests-roles.test.ts index f75c8e20e..db2de4d15 100644 --- a/lambda/__tests__/05.requests-roles.test.ts +++ b/lambda/__tests__/05.requests-roles.test.ts @@ -27,14 +27,6 @@ const integrationRoles = [ }, ]; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/06.email-templates.test.ts b/lambda/__tests__/06.email-templates.test.ts index 184130043..eed786251 100644 --- a/lambda/__tests__/06.email-templates.test.ts +++ b/lambda/__tests__/06.email-templates.test.ts @@ -17,10 +17,6 @@ import { cleanUpDatabaseTables, createMockAuth } from './helpers/utils'; const MOCK_PRIVACY_ZONE_URI = 'zone'; -jest.mock('@lambda-app/authenticate'); - -jest.mock('@lambda-shared/utils/ches'); - jest.mock('@lambda-shared/templates/helpers', () => { const original = jest.requireActual('@lambda-shared/templates/helpers'); return { @@ -343,6 +339,7 @@ describe('Email template snapshots', () => { clientId: 'test-client', roles: 'test-role', teamAdmin: true, + env: 'dev', }); expect(rendered.subject).toMatchSnapshot(); expect(rendered.body).toMatchSnapshot(); diff --git a/lambda/__tests__/07.emails-for-user-requests.test.ts b/lambda/__tests__/07.emails-for-user-requests.test.ts index d25e5be31..eae98a4b8 100644 --- a/lambda/__tests__/07.emails-for-user-requests.test.ts +++ b/lambda/__tests__/07.emails-for-user-requests.test.ts @@ -13,8 +13,6 @@ import { EMAILS } from '@lambda-shared/enums'; import { IDIM_EMAIL_ADDRESS, SSO_EMAIL_ADDRESS } from '@lambda-shared/local'; import { buildIntegration } from './helpers/modules/common'; -jest.mock('../app/src/authenticate'); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { @@ -23,8 +21,6 @@ jest.mock('../app/src/keycloak/integration', () => { }; }); -jest.mock('@lambda-shared/utils/ches'); - jest.mock('../app/src/keycloak/client', () => { return { disableIntegration: jest.fn(() => Promise.resolve()), diff --git a/lambda/__tests__/08.emails-for-team-requests.test.ts b/lambda/__tests__/08.emails-for-team-requests.test.ts index 37f146ba6..e6e4fc5bd 100644 --- a/lambda/__tests__/08.emails-for-team-requests.test.ts +++ b/lambda/__tests__/08.emails-for-team-requests.test.ts @@ -19,8 +19,6 @@ import { buildIntegration } from './helpers/modules/common'; import { getAuthenticatedUser } from './helpers/modules/users'; import { generateInvitationToken } from '@lambda-app/helpers/token'; -jest.mock('../app/src/authenticate'); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { @@ -29,8 +27,6 @@ jest.mock('../app/src/keycloak/integration', () => { }; }); -jest.mock('@lambda-shared/utils/ches'); - jest.mock('../app/src/keycloak/client', () => { return { disableIntegration: jest.fn(() => Promise.resolve()), diff --git a/lambda/__tests__/09.emails-for-teams.test.ts b/lambda/__tests__/09.emails-for-teams.test.ts index a4de4db04..a45e296c7 100644 --- a/lambda/__tests__/09.emails-for-teams.test.ts +++ b/lambda/__tests__/09.emails-for-teams.test.ts @@ -12,8 +12,6 @@ import { createTeam, deleteMembersOfTeam, deleteTeam, getMembersOfTeam } from '. const TEST_TOKEN = 'testtoken'; -jest.mock('@lambda-app/authenticate'); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { @@ -29,7 +27,6 @@ jest.mock('@lambda-app/helpers/token', () => { generateInvitationToken: jest.fn(() => TEST_TOKEN), }; }); -jest.mock('@lambda-shared/utils/ches'); describe('emails for teams', () => { afterAll(async () => { diff --git a/lambda/__tests__/10.team-api-account.test.ts b/lambda/__tests__/10.team-api-account.test.ts index 0be320faf..b45c7f3f8 100644 --- a/lambda/__tests__/10.team-api-account.test.ts +++ b/lambda/__tests__/10.team-api-account.test.ts @@ -6,6 +6,7 @@ import app from './helpers/server'; import { API_BASE_PATH } from './helpers/constants'; import { buildIntegration } from './helpers/modules/common'; import { findClientRole } from '@lambda-app/keycloak/users'; +import { KeycloakService } from '@lambda-css-api/services/keycloak-service'; let team; let integration; @@ -70,8 +71,6 @@ const deleteUserRoleMapping = { const mockedFindClientRole = findClientRole as jest.Mock; -jest.mock('@lambda-app/authenticate'); - jest.mock('@lambda-app/keycloak/users', () => { return { listClientRoles: jest.fn(() => { @@ -110,7 +109,6 @@ jest.mock('@lambda-app/helpers/token', () => { generateInvitationToken: jest.fn(() => TEST_TOKEN), }; }); -jest.mock('@lambda-shared/utils/ches'); jest.mock('@lambda-app/authenticate'); @@ -144,13 +142,6 @@ jest.mock('@lambda-css-api/authenticate', () => { }); const createIntegrationRoles = async (roleName: string, intId: number, env: string) => { - mockedFindClientRole.mockImplementationOnce(() => { - return Promise.resolve({ - name: roleName, - composite: false, - }); - }); - return await supertest(app) .post(`${API_BASE_PATH}/integrations/${intId}/dev/roles`) .send({ name: roleName }) @@ -161,6 +152,10 @@ describe('emails for teams', () => { beforeAll(async () => { jest.clearAllMocks(); createMockAuth(TEAM_ADMIN_IDIR_USERID_01, TEAM_ADMIN_IDIR_EMAIL_01); + jest.spyOn(KeycloakService.prototype, 'setEnvironment').mockImplementation(() => { + return Promise.resolve(); + }); + const result = await createTeam(postTeam); expect(result.status).toEqual(200); team = result.body; @@ -171,10 +166,15 @@ describe('emails for teams', () => { teamId: team.id, }); integration = integrationRes.body; - await createIntegrationRoles('role1', integration.id, 'dev'); - await createIntegrationRoles('role2', integration.id, 'dev'); - await createIntegrationRoles('role3', integration.id, 'dev'); + + ['role1', 'role2', 'role3'].forEach(async (role) => { + jest.spyOn(KeycloakService.prototype, 'createClientRole').mockImplementationOnce(() => { + return Promise.resolve({ name: role, composite: false }); + }); + await createIntegrationRoles(role, integration.id, 'dev'); + }); }); + afterAll(async () => { await cleanUpDatabaseTables(); }); @@ -223,34 +223,36 @@ describe('emails for teams', () => { }); it('creates team integration role for an environment', async () => { + const createClientRoleMock = jest.spyOn(KeycloakService.prototype, 'createClientRole').mockImplementation(() => { + return Promise.resolve({ name: createIntegrationRole, composite: false }); + }); const result = await createIntegrationRoles(createIntegrationRole, integration.id, 'dev'); + expect(createClientRoleMock).toHaveBeenCalled(); expect(result.body.name).toBe(createIntegrationRole); expect(result.body.composite).toBe(false); }); it('updates team integration role for an environment', async () => { - mockedFindClientRole.mockImplementationOnce(() => { - return Promise.resolve({ - name: 'role5', - composite: false, - }); + const updateClientRoleMock = jest.spyOn(KeycloakService.prototype, 'updateClientRole').mockImplementation(() => { + return Promise.resolve({ name: 'role5', composite: false }); }); const result = await supertest(app) .put(`${API_BASE_PATH}/integrations/${integration.id}/dev/roles/${createIntegrationRole}`) .send({ name: updateIntegrationRole }) .set('Accept', 'application/json') .expect(200); + + expect(updateClientRoleMock).toHaveBeenCalled(); expect(result.body.name).toBe(updateIntegrationRole); expect(result.body.composite).toBe(false); }); it('create composite role', async () => { - mockedFindClientRole.mockImplementationOnce(() => { - return Promise.resolve({ - name: 'role11', - composite: true, + const createCompositeRoleMock = jest + .spyOn(KeycloakService.prototype, 'createCompositeRole') + .mockImplementation(() => { + return Promise.resolve({ name: 'role1', composite: true }); }); - }); const result = await supertest(app) .post(`${API_BASE_PATH}/integrations/${integration.id}/dev/roles/role1/composite-roles`) @@ -258,15 +260,20 @@ describe('emails for teams', () => { .set('Accept', 'application/json') .expect(200); - expect(result.body.name).toBe('role11'); + expect(createCompositeRoleMock).toHaveBeenCalled(); + expect(result.body.name).toBe('role1'); expect(result.body.composite).toBe(true); }); it('get role composites', async () => { + const getCompositeRolesMock = jest.spyOn(KeycloakService.prototype, 'getCompositeRoles').mockImplementation(() => { + return Promise.resolve([{ name: 'role2', composite: false }]); + }); const result = await supertest(app) .get(`${API_BASE_PATH}/integrations/${integration.id}/dev/roles/role1/composite-roles`) .expect(200); + expect(getCompositeRolesMock).toHaveBeenCalled(); result.body.data.forEach((role) => { expect(role.name).toBe('role2'); expect(role.composite).toBe(false); @@ -274,15 +281,20 @@ describe('emails for teams', () => { }); it('get role composite', async () => { + const getCompositeRoleMock = jest.spyOn(KeycloakService.prototype, 'getCompositeRoles').mockImplementation(() => { + return Promise.resolve({ name: 'role2', composite: false }); + }); const result = await supertest(app) .get(`${API_BASE_PATH}/integrations/${integration.id}/dev/roles/role1/composite-roles/role2`) .expect(200); + expect(getCompositeRoleMock).toHaveBeenCalled(); expect(result.body.name).toBe('role2'); expect(result.body.composite).toBe(false); }); it('remove role composite', async () => { + jest.spyOn(KeycloakService.prototype, 'deleteCompositeRole').mockImplementation(); await supertest(app) .delete(`${API_BASE_PATH}/integrations/${integration.id}/dev/roles/role1/composite-roles/role2`) .expect(204); @@ -392,6 +404,7 @@ describe('emails for teams', () => { }); it('deletes team integration role for an environment', async () => { + jest.spyOn(KeycloakService.prototype, 'deleteClientRole').mockImplementation(); await supertest(app) .delete(`${API_BASE_PATH}/integrations/${integration.id}/dev/roles/${updateIntegrationRole}`) .expect(204); @@ -423,11 +436,15 @@ describe('emails for teams', () => { }); it('gets team integration role mappings for an environment when username is supplied', async () => { + const listClientUserRoleMappingsMock = jest + .spyOn(KeycloakService.prototype, 'listClientUserRoleMappings') + .mockImplementation(() => Promise.resolve(integrationUserRoles)); const result = await supertest(app) .get(`${API_BASE_PATH}/integrations/${integration.id}/dev/user-role-mappings`) .query({ username: integrationRoleUsers[0].username }) .expect(200); + expect(listClientUserRoleMappingsMock).toHaveBeenCalled(); expect(result.body.roles.length > 0).toBe(true); expect(result.body.roles[0].name).toBe(integrationUserRoles[0].name); expect(result.body.roles[0].composite).toBe(integrationUserRoles[0].composite); @@ -472,20 +489,42 @@ describe('emails for teams', () => { }); it('gets roles associated with user for an environment when username is supplied', async () => { + const listClientUserRoleMappingsMock = jest + .spyOn(KeycloakService.prototype, 'listClientUserRoleMappings') + .mockImplementation(() => Promise.resolve(integrationUserRoles)); const result = await supertest(app) .get(`${API_BASE_PATH}/integrations/${integration.id}/dev/users/${createUserRoleMapping.username}/roles`) .expect(200); + expect(listClientUserRoleMappingsMock).toHaveBeenCalled(); expect(result.body.data.length > 0).toBe(true); expect(result.body.data[0].name).toBe(integrationUserRoles[0].name); expect(result.body.data[0].composite).toBe(integrationUserRoles[0].composite); }); + it('gets roles associated with a service account for an environment when the client id is supplied as username', async () => { + const listClientUserRoleMappingsMock = jest + .spyOn(KeycloakService.prototype, 'listClientUserRoleMappings') + .mockImplementation(() => Promise.resolve(integrationUserRoles)); + await supertest(app) + .get(`${API_BASE_PATH}/integrations/${integration.id}/dev/users/${integration.clientId}/roles`) + .expect(200); + + expect(listClientUserRoleMappingsMock).toHaveBeenCalledWith( + integration.clientId, + `service-account-${integration.clientId}`, + ); + }); + it('gets users associated to a role per page for an environment when roleName is supplied', async () => { + const listUsersByClientRoleMock = jest + .spyOn(KeycloakService.prototype, 'listUsersByClientRole') + .mockImplementation(() => Promise.resolve(integrationRoleUsers)); const result = await supertest(app) .get(`${API_BASE_PATH}/integrations/${integration.id}/dev/roles/${integrationUserRoles[0].name}/users`) .expect(200); + expect(listUsersByClientRoleMock).toHaveBeenCalled(); expect(result.body.data.length > 0).toBe(true); expect(result.body.page).toBe(1); expect(result.body.data[0].username).toBe(integrationRoleUsers[0].username); @@ -496,20 +535,62 @@ describe('emails for teams', () => { }); it('assign a role to an user for an environment', async () => { + const addClientUserRoleMappingMock = jest + .spyOn(KeycloakService.prototype, 'addClientUserRoleMapping') + .mockImplementation(() => Promise.resolve([{ name: 'role1', composite: false }])); const result = await supertest(app) .post(`${API_BASE_PATH}/integrations/${integration.id}/dev/users/${createUserRoleMapping.username}/roles`) .send([{ name: 'role1' }]) .set('Accept', 'application/json') .expect(201); + + expect(addClientUserRoleMappingMock).toHaveBeenCalled(); expect(result.body.data.length > 0).toBe(true); expect(result.body.data[0].name).toBe(integrationUserRoles[0].name); expect(result.body.data[0].composite).toBe(integrationUserRoles[0].composite); }); + it('Remaps client id to service account username on role assignment', async () => { + const addClientUserRoleMappingMock = jest + .spyOn(KeycloakService.prototype, 'addClientUserRoleMapping') + .mockImplementation(() => Promise.resolve([{ name: 'role1', composite: false }])); + await supertest(app) + .post(`${API_BASE_PATH}/integrations/${integration.id}/dev/users/${integration.clientId}/roles`) + .send([{ name: 'role1' }]) + .set('Accept', 'application/json') + .expect(201); + + expect(addClientUserRoleMappingMock).toHaveBeenCalledWith( + integration.clientId, + `service-account-${integration.clientId}`, + [{ name: 'role1' }], + ); + }); + it('unassign a role to an user for an environment', async () => { + const deleteClientUserRoleMappingMock = jest + .spyOn(KeycloakService.prototype, 'deleteClientUserRoleMapping') + .mockImplementation(() => Promise.resolve(null)); const result = await supertest(app) .delete(`${API_BASE_PATH}/integrations/${integration.id}/dev/users/${createUserRoleMapping.username}/roles/role1`) .expect(204); + expect(deleteClientUserRoleMappingMock).toHaveBeenCalled(); expect(result.body).toBeNull; }); + + it('Remaps the client ID to the service account name on deletes', async () => { + const deleteClientUserRoleMappingMock = jest + .spyOn(KeycloakService.prototype, 'deleteClientUserRoleMapping') + .mockImplementation(() => Promise.resolve(null)); + + await supertest(app) + .delete(`${API_BASE_PATH}/integrations/${integration.id}/dev/users/${integration.clientId}/roles/role1`) + .expect(204); + + expect(deleteClientUserRoleMappingMock).toHaveBeenCalledWith( + integration.clientId, + `service-account-${integration.clientId}`, + 'role1', + ); + }); }); diff --git a/lambda/__tests__/11.remove-inactive-users.test.ts b/lambda/__tests__/11.remove-inactive-users.test.ts index ea4f32662..0379e05b1 100644 --- a/lambda/__tests__/11.remove-inactive-users.test.ts +++ b/lambda/__tests__/11.remove-inactive-users.test.ts @@ -17,6 +17,7 @@ import { models } from '@lambda-shared/sequelize/models/models'; import { EMAILS } from '@lambda-shared/enums'; import { renderTemplate } from '@lambda-shared/templates'; import { SSO_EMAIL_ADDRESS } from '@lambda-shared/local'; +import { deleteIntegration } from './helpers/modules/integrations'; const testUser = { username: TEAM_ADMIN_IDIR_USERNAME_01, @@ -26,16 +27,9 @@ const testUser = { idir_username: TEAM_ADMIN_IDIR_USERNAME_01, }, clientData: {}, + env: 'prod', }; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { @@ -121,6 +115,7 @@ describe('users and teams', () => { clientId: testUser.clientData[0].client, roles: testUser.clientData[0].roles[0], teamAdmin: true, + env: 'prod', }); expect(deleteResponse.status).toEqual(200); const user = await models.user.findOne({ where: { idir_userid: TEAM_ADMIN_IDIR_USERID_01 }, raw: true }); @@ -257,4 +252,132 @@ describe('Deleted user emails', () => { expect(deleteInactiveIntegrationEmails[0].body.includes('role2')).toBeTruthy(); expect(orphanedIntegrationEmails.length).toBe(0); }); + + it('Skips the notification if the integration has been archived', async () => { + const emailList = createMockSendEmail(); + + // Create a team and integration + const adminTeam = await createTeam({ + name: 'test_team', + members: [ + { + idirEmail: TEAM_MEMBER_IDIR_EMAIL_01, + role: 'admin', + }, + ], + }); + const request = await buildIntegration({ + projectName: 'Delete Inactive Users', + bceid: false, + prodEnv: false, + submitted: true, + teamId: adminTeam.body.id, + }); + // Archive the integration + await deleteIntegration(request.body.id); + + // Call the endpoint with no roles, should be no email since deleted + await deleteInactiveUsers(testUser); + let deleteInactiveIntegrationEmails = emailList.filter((email) => email.code === EMAILS.DELETE_INACTIVE_IDIR_USER); + let orphanedIntegrationEmails = emailList.filter((email) => email.code === EMAILS.ORPHAN_INTEGRATION); + + expect(orphanedIntegrationEmails.length).toBe(0); + expect(deleteInactiveIntegrationEmails.length).toBe(0); + + jest.clearAllMocks(); + + // Call the user deletion endpoint with client roles, expect no email since deleted + testUser.clientData = [{ client: request.body.clientId, roles: ['role1', 'role2'] }]; + await deleteInactiveUsers(testUser); + + deleteInactiveIntegrationEmails = emailList.filter((email) => email.code === EMAILS.DELETE_INACTIVE_IDIR_USER); + orphanedIntegrationEmails = emailList.filter((email) => email.code === EMAILS.ORPHAN_INTEGRATION); + + expect(orphanedIntegrationEmails.length).toBe(0); + expect(deleteInactiveIntegrationEmails.length).toBe(0); + }); +}); + +describe('Environment Check', () => { + beforeAll(async () => { + await cleanUpDatabaseTables(); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await cleanUpDatabaseTables(); + }); + + beforeEach(async () => { + await models.user.create({ idirUserid: SSO_TEAM_IDIR_USER, idirEmail: SSO_TEAM_IDIR_EMAIL }); + await models.user.create({ idirUserid: TEAM_ADMIN_IDIR_USERID_01, idirEmail: TEAM_ADMIN_IDIR_EMAIL_01 }); + createMockAuth(SSO_TEAM_IDIR_USER, SSO_TEAM_IDIR_EMAIL); + }); + + it('Only removes users in CSS when they are deleted from the production environment', async () => { + testUser.env = 'dev'; + await deleteInactiveUsers(testUser); + let user = await models.user.findOne({ where: { idir_email: testUser.email }, raw: true }); + expect(user).not.toBeNull(); + + testUser.env = 'test'; + await deleteInactiveUsers(testUser); + user = await models.user.findOne({ where: { idir_email: testUser.email }, raw: true }); + expect(user).not.toBeNull(); + + testUser.env = 'prod'; + await deleteInactiveUsers(testUser); + user = await models.user.findOne({ where: { idir_email: testUser.email }, raw: true }); + expect(user).toBeNull(); + }); + + it('Sends role information for all environments, and team admin information only for production', async () => { + let emailList = createMockSendEmail(); + const adminTeam = await createTeam({ + name: 'test_team', + members: [ + { + idirEmail: TEAM_ADMIN_IDIR_EMAIL_01, + role: 'admin', + }, + ], + }); + const request = await buildIntegration({ + projectName: 'Delete Inactive Users', + bceid: false, + prodEnv: false, + submitted: true, + teamId: adminTeam.body.id, + }); + + for (const env of ['dev', 'test', 'prod']) { + // Reset mocks between env tests + if (emailList.length) { + jest.clearAllMocks(); + createMockAuth(SSO_TEAM_IDIR_USER, SSO_TEAM_IDIR_EMAIL); + emailList = createMockSendEmail(); + } + + testUser.env = env; + testUser.clientData = [{ client: request.body.clientId, roles: ['role1', 'role2'] }]; + await deleteInactiveUsers(testUser); + + const deleteInactiveIntegrationEmails = emailList.filter( + (email) => email.code === EMAILS.DELETE_INACTIVE_IDIR_USER, + ); + expect(deleteInactiveIntegrationEmails.length).toBe(1); + + // Only does team admin notification for prod users + if (env === 'prod') { + expect(deleteInactiveIntegrationEmails[0].body.includes('Team Admin')).toBeTruthy(); + } else { + expect(deleteInactiveIntegrationEmails[0].body.includes('Team Admin')).not.toBeTruthy(); + } + + // Always sends role information + expect(deleteInactiveIntegrationEmails[0].body.includes('role1')).toBeTruthy(); + expect(deleteInactiveIntegrationEmails[0].body.includes('role2')).toBeTruthy(); + expect(deleteInactiveIntegrationEmails[0].body.includes(env)).toBeTruthy(); + } + }); }); diff --git a/lambda/__tests__/12.submit-survey.test.ts b/lambda/__tests__/12.submit-survey.test.ts index fd52c0a9b..414d078f1 100644 --- a/lambda/__tests__/12.submit-survey.test.ts +++ b/lambda/__tests__/12.submit-survey.test.ts @@ -9,13 +9,6 @@ const surveyData = { triggerEvent: 'addUserToRole', }; -jest.mock('../app/src/authenticate'); -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/13.profile.test.ts b/lambda/__tests__/13.profile.test.ts index 4c85b844a..0b60bb308 100644 --- a/lambda/__tests__/13.profile.test.ts +++ b/lambda/__tests__/13.profile.test.ts @@ -2,13 +2,6 @@ import { cleanUpDatabaseTables, createMockAuth } from './helpers/utils'; import { SSO_TEAM_IDIR_EMAIL, SSO_TEAM_IDIR_USER } from './helpers/fixtures'; import { getAuthenticatedUser, updateProfile } from './helpers/modules/users'; -jest.mock('../app/src/authenticate'); -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/14.digital-credential.test.ts b/lambda/__tests__/14.digital-credential.test.ts index b576efd78..2717c928a 100644 --- a/lambda/__tests__/14.digital-credential.test.ts +++ b/lambda/__tests__/14.digital-credential.test.ts @@ -8,14 +8,6 @@ import { DIT_ADDITIONAL_EMAIL_ADDRESS, DIT_EMAIL_ADDRESS } from '@lambda-shared/ import { submitNewIntegration, updateIntegration } from './helpers/modules/integrations'; import { EMAILS } from '@lambda-shared/enums'; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/15.sso-logs.test.ts b/lambda/__tests__/15.sso-logs.test.ts index cbef66471..0df0098dc 100644 --- a/lambda/__tests__/15.sso-logs.test.ts +++ b/lambda/__tests__/15.sso-logs.test.ts @@ -1,13 +1,13 @@ import app from './helpers/server'; import supertest from 'supertest'; -import { APP_BASE_PATH } from './helpers/constants'; +import { API_BASE_PATH, APP_BASE_PATH } from './helpers/constants'; import { cleanUpDatabaseTables, createMockAuth } from './helpers/utils'; import { getUpdateIntegrationData } from './helpers/fixtures'; import { models } from '@lambda-shared/sequelize/models/models'; -import { queryGrafana } from '../app/src/grafana'; +import { queryGrafana } from '@lambda-app/grafana'; import { EVENTS } from '@lambda-shared/enums'; +import * as rateLimiters from '@lambda-app/utils/rate-limiters'; -jest.mock('../app/src/authenticate'); jest.mock('../app/src/grafana', () => { return { queryGrafana: jest.fn(() => Promise.resolve(['{"log": "log"}', '{"log": "log"}'])), @@ -21,31 +21,44 @@ jest.mock('../app/src/keycloak/integration', () => { keycloakClient: jest.fn(() => Promise.resolve(true)), }; }); -describe('Fetch SSO Logs', () => { - const integration = getUpdateIntegrationData({ integration: { projectName: 'test_project' } }); +const AUTHENTICATED_TEAM_ID = 1; +const UNAUTHENTICATED_TEAM_ID = 2; +const teamAuthResponse = { + success: true, + data: { + teamId: AUTHENTICATED_TEAM_ID, + }, +}; + +jest.mock('@lambda-css-api/authenticate', () => ({ + authenticate: jest.fn(() => Promise.resolve(teamAuthResponse)), +})); + +const MOCK_USER_ID = -1; +const MOCK_USER_EMAIL = 'test@user.com'; +const INTEGRATION_ID = -1; +const integration = getUpdateIntegrationData({ integration: { projectName: 'test_project' } }); + +// Create integration owned by mock user to test against +const setupIntegrationAndUser = async () => { + await models.user.create({ + id: MOCK_USER_ID, + idirEmail: MOCK_USER_EMAIL, + }); + await models.request.create({ + ...integration, + id: INTEGRATION_ID, + usesTeam: false, + userId: MOCK_USER_ID, + }); +}; + +describe('Fetch SSO Logs', () => { afterEach(async () => { await cleanUpDatabaseTables(); }); - const MOCK_USER_ID = -1; - const MOCK_USER_EMAIL = 'test@user.com'; - const INTEGRATION_ID = -1; - - // Create integration owned by mock user to test against - const setupIntegrationAndUser = async () => { - await models.user.create({ - id: MOCK_USER_ID, - idirEmail: MOCK_USER_EMAIL, - }); - await models.request.create({ - ...integration, - id: INTEGRATION_ID, - usesTeam: false, - userId: MOCK_USER_ID, - }); - }; - // A valid query string to use const queryString = `env=dev&start=2020-10-10&end=2020-10-12`; @@ -57,7 +70,7 @@ describe('Fetch SSO Logs', () => { .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`) .set('Accept', 'application/json'); - expect(response.status).toBe(401); + expect(response.status).toBe(403); // Create session with actual user, expect 200 createMockAuth('2', MOCK_USER_EMAIL); @@ -108,7 +121,7 @@ describe('Fetch SSO Logs', () => { .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`) .set('Accept', 'application/json'); - expect(response.status).toBe(401); + expect(response.status).toBe(403); }); it('Responds with 400 if supplied query parameters are wrong', async () => { @@ -126,14 +139,16 @@ describe('Fetch SSO Logs', () => { 'env=dev&start=2022-10-10&end=anotherDate', // Range too large 'env=dev&start=2022-10-10&end=2022-12-12', + // Start date later than end date + 'env=dev&start=2022-10-02&end=2022-10-01', ]; const validParams = [ - 'env=dev&start=2022-10-10&end=2022-10-10', - 'env=test&start=2022-10-10&end=2022-10-10', - 'env=prod&start=2022-10-10&end=2022-10-10', + 'env=dev&start=2022-10-10&end=2022-10-11', + 'env=test&start=2022-10-10&end=2022-10-11', + 'env=prod&start=2022-10-10&end=2022-10-11', 'env=dev&start=2022-10-10&end=2022-10-10T12:10:10', - 'env=dev&start=2022-12-10T10:10:10&end=2022-10-10T10:10:10', + 'env=dev&start=2022-10-10T10:10:10&end=2022-10-10T10:10:11', ]; createMockAuth('2', MOCK_USER_EMAIL); @@ -187,4 +202,72 @@ describe('Fetch SSO Logs', () => { }); expect(event).not.toBeNull(); }); + + it('Return 429 if too many requests', async () => { + await setupIntegrationAndUser(); + jest.spyOn(rateLimiters, 'logsRateLimiter').mockImplementationOnce((req, res) => { + res.status(429).send({ message: 'Rate limit exceeded' }); + }); + const response = await supertest(app) + .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`) + .set('Accept', 'application/json'); + + expect(response.status).toBe(429); + }); +}); + +describe('CSS API - Fetch Logs', () => { + const integration = getUpdateIntegrationData({ integration: { projectName: 'test_project' } }); + + afterEach(async () => { + await cleanUpDatabaseTables(); + }); + + const createTeamOwnedIntegration = async (teamId: number) => { + await models.team.create({ + id: teamId, + name: 'test_team', + }); + const createdIntegration = await models.request.create({ + ...integration, + usesTeam: true, + teamId, + }); + return createdIntegration.id; + }; + + it("Allows team service account to fetch its own integration's logs", async () => { + const integrationId = await createTeamOwnedIntegration(AUTHENTICATED_TEAM_ID); + + const queryString = `start=2020-10-10&end=2020-10-12`; + const response = await supertest(app) + .get(`${API_BASE_PATH}/integrations/${integrationId}/dev/logs?${queryString}`) + .set('Accept', 'application/json'); + + expect(response.status).toBe(200); + }); + + it('Rejects request if account is not associated with the owning team', async () => { + // Create integration owned by another team + const unauthorizedIntegrationId = await createTeamOwnedIntegration(UNAUTHENTICATED_TEAM_ID); + const queryString = `start=2020-10-10&end=2020-10-12`; + const response = await supertest(app) + .get(`${API_BASE_PATH}/integrations/${unauthorizedIntegrationId}/dev/logs?${queryString}`) + .set('Accept', 'application/json'); + + expect(response.status).toBe(403); + }); + + it('Return 429 if too many requests', async () => { + await setupIntegrationAndUser(); + const queryString = `start=2020-10-10&end=2020-10-12`; + jest.spyOn(rateLimiters, 'logsRateLimiter').mockImplementationOnce((req, res) => { + res.status(429).send({ message: 'Rate limit exceeded' }); + }); + const response = await supertest(app) + .get(`${APP_BASE_PATH}/requests/${INTEGRATION_ID}/logs?${queryString}`) + .set('Accept', 'application/json'); + + expect(response.status).toBe(429); + }); }); diff --git a/lambda/__tests__/16.integration-metrics.test.ts b/lambda/__tests__/16.integration-metrics.test.ts index 3bd74a537..c550ed8fc 100644 --- a/lambda/__tests__/16.integration-metrics.test.ts +++ b/lambda/__tests__/16.integration-metrics.test.ts @@ -10,8 +10,6 @@ jest.mock('../app/src/grafana', () => { }; }); -jest.mock('@lambda-app/authenticate'); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { @@ -20,8 +18,6 @@ jest.mock('../app/src/keycloak/integration', () => { }; }); -jest.mock('@lambda-shared/utils/ches'); - describe('create/manage integration by authenticated user', () => { let integration: Integration; try { diff --git a/lambda/__tests__/17.run-queued-requests.test.ts b/lambda/__tests__/17.run-queued-requests.test.ts index 3bfdb7699..4afe5aebe 100644 --- a/lambda/__tests__/17.run-queued-requests.test.ts +++ b/lambda/__tests__/17.run-queued-requests.test.ts @@ -11,11 +11,10 @@ import { } from './helpers/modules/integrations'; import { ACTION_TYPES, EMAILS, EVENTS } from '@lambda-shared/enums'; import { QUEUE_ACTION } from '@lambda-shared/interfaces'; -import { handler } from '../request-queue/src/main'; +import { handler, MAX_ATTEMPTS } from '../request-queue/src/main'; import { createEvent, standardClients } from '@lambda-app/controllers/requests'; jest.mock('@lambda-app/keycloak/adminClient'); -jest.mock('@lambda-shared/utils/ches'); describe('Request Queue', () => { beforeEach(async () => { @@ -105,24 +104,68 @@ describe('Request Queue', () => { expect(email.to.includes(formDataProd.user.idirEmail)).toBeTruthy(); }); - it('Keeps item in the queue if any environments fail and updates request status to failed', async () => { + it('Keeps item in the queue if any environments fail, updates request status to failed and increments attempt counter', async () => { const kcClientSpy = jest.spyOn(IntegrationModule, 'keycloakClient'); await createRequestQueueItem(1, formDataProd, ACTION_TYPES.CREATE as QUEUE_ACTION, 61); const request = await generateRequest(formDataProd); + let queueItems = await getQueueItems(); + expect(queueItems.length).toBe(1); + expect(queueItems[0].attempts).toBe(0); + // Have one environment fail kcClientSpy.mockResolvedValueOnce(true).mockResolvedValueOnce(false).mockResolvedValueOnce(true); await handler(); - const queueItems = await getQueueItems(); + queueItems = await getQueueItems(); expect(queueItems.length).toBe(1); + expect(queueItems[0].attempts).toBe(1); const updatedRequest = await getRequest(request.id); expect(updatedRequest.status).toBe('applyFailed'); kcClientSpy.mockRestore(); }); + + it('Ignores queue item if at max attempts', async () => { + const kcClientSpy = jest.spyOn(IntegrationModule, 'keycloakClient'); + (axios.post as jest.Mock).mockImplementation(() => Promise.resolve({ data: [] })); + await createRequestQueueItem(1, formDataProd, ACTION_TYPES.CREATE as QUEUE_ACTION, 61, MAX_ATTEMPTS); + + await handler(); + + const queueItems = await getQueueItems(); + expect(queueItems.length).toBe(1); + expect(queueItems[0].attempts).toBe(MAX_ATTEMPTS); + expect(kcClientSpy).not.toHaveBeenCalled(); + // No rocket chat calls + expect(axios.post).not.toHaveBeenCalled(); + kcClientSpy.mockRestore(); + }); + + it('Sends a notification to rocket chat if max attempts is reached', async () => { + const kcClientSpy = jest.spyOn(IntegrationModule, 'keycloakClient'); + // Reject all + kcClientSpy.mockImplementation(() => Promise.resolve(false)); + (axios.post as jest.Mock).mockImplementation(() => Promise.resolve({ data: [] })); + + await createRequestQueueItem(1, formDataProd, ACTION_TYPES.CREATE as QUEUE_ACTION, 61, MAX_ATTEMPTS - 1); + await handler(); + + const queueItems = await getQueueItems(); + expect(queueItems.length).toBe(1); + expect(queueItems[0].attempts).toBe(MAX_ATTEMPTS); + expect(kcClientSpy).toHaveBeenCalledTimes(3); + + expect(axios.post).toHaveBeenCalledTimes(1); + // Check rocket chat message + const [_axiosUrl, axiosBody] = (axios.post as jest.Mock).mock.calls[0]; + expect(axiosBody.message).toBe( + `SANDBOX: Request ${formDataProd.clientId} has reached maximum retries and requires manual intervention.`, + ); + kcClientSpy.mockRestore(); + }); }); describe('Delete and Update', () => { diff --git a/lambda/__tests__/18.request-monitor.test.ts b/lambda/__tests__/18.request-monitor.test.ts index 0f5f725f7..a3e1107d4 100644 --- a/lambda/__tests__/18.request-monitor.test.ts +++ b/lambda/__tests__/18.request-monitor.test.ts @@ -4,14 +4,6 @@ import { buildIntegration } from './helpers/modules/common'; import { cleanUpDatabaseTables, createMockAuth } from './helpers/utils'; import * as requestMonitorModule from '../request-monitor/src/main'; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/19.idir-search.test.ts b/lambda/__tests__/19.idir-search.test.ts index 6ba29ff0d..e5a4da26e 100644 --- a/lambda/__tests__/19.idir-search.test.ts +++ b/lambda/__tests__/19.idir-search.test.ts @@ -3,8 +3,6 @@ import { TEAM_ADMIN_IDIR_EMAIL_01, TEAM_ADMIN_IDIR_USERID_01 } from './helpers/f import { searchIdirUsers } from './helpers/modules/users'; import { searchIdirEmail } from '@lambda-app/ms-graph/idir'; -jest.mock('../app/src/authenticate'); - const AZURE_RESPONSE = ['some.user@email.com']; jest.mock('../app/src/ms-graph/idir', () => { diff --git a/lambda/__tests__/20.bcsc.test.ts b/lambda/__tests__/20.bcsc.test.ts index 534e5ed0d..a4e89b4c4 100644 --- a/lambda/__tests__/20.bcsc.test.ts +++ b/lambda/__tests__/20.bcsc.test.ts @@ -26,15 +26,12 @@ jest.mock('@lambda-app/controllers/bc-services-card', () => { }; }); -jest.mock('../app/src/authenticate'); - jest.mock('@lambda-app/keycloak/adminClient', () => { return { getAdminClient: jest.fn(() => Promise.resolve({})), }; }); -jest.mock('@lambda-shared/utils/ches'); jest.mock('@lambda-app/bcsc/client', () => { const original = jest.requireActual('@lambda-app/bcsc/client'); return { diff --git a/lambda/__tests__/21.requests-validations.test.ts b/lambda/__tests__/21.requests-validations.test.ts index ccb161884..ab1540580 100644 --- a/lambda/__tests__/21.requests-validations.test.ts +++ b/lambda/__tests__/21.requests-validations.test.ts @@ -16,8 +16,6 @@ import { buildIntegration } from './helpers/modules/common'; import { getAuthenticatedUser } from './helpers/modules/users'; import { generateInvitationToken } from '@lambda-app/helpers/token'; -jest.mock('../app/src/authenticate'); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { @@ -26,8 +24,6 @@ jest.mock('../app/src/keycloak/integration', () => { }; }); -jest.mock('@lambda-shared/utils/ches'); - jest.mock('../app/src/keycloak/client', () => { return { disableIntegration: jest.fn(() => Promise.resolve()), diff --git a/lambda/__tests__/22.sso-admin.test.ts b/lambda/__tests__/22.sso-admin.test.ts index 658ebb816..39e523586 100644 --- a/lambda/__tests__/22.sso-admin.test.ts +++ b/lambda/__tests__/22.sso-admin.test.ts @@ -8,14 +8,6 @@ import { buildIntegration } from './helpers/modules/common'; import { listClientCompositeRoles, listClientRoles, listClientRoleUsers } from './helpers/modules/roles'; import { createMockAuth, cleanUpDatabaseTables } from './helpers/utils'; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/23.search-requests.test.ts b/lambda/__tests__/23.search-requests.test.ts index f6124c608..3a91c4e64 100644 --- a/lambda/__tests__/23.search-requests.test.ts +++ b/lambda/__tests__/23.search-requests.test.ts @@ -3,14 +3,6 @@ import { getListOfIntegrations } from './helpers/modules/integrations'; import { cleanUpDatabaseTables, createMockAuth } from './helpers/utils'; import { buildIntegration } from './helpers/modules/common'; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/25.idp-approver.test.ts b/lambda/__tests__/25.idp-approver.test.ts index 6eeea3df3..c22bcd5f2 100644 --- a/lambda/__tests__/25.idp-approver.test.ts +++ b/lambda/__tests__/25.idp-approver.test.ts @@ -19,14 +19,6 @@ import { } from './helpers/modules/integrations'; import { cleanUpDatabaseTables, createMockAuth } from './helpers/utils'; -jest.mock('../app/src/authenticate'); - -jest.mock('../shared/utils/ches', () => { - return { - sendEmail: jest.fn(), - }; -}); - jest.mock('../app/src/keycloak/integration', () => { const original = jest.requireActual('../app/src/keycloak/integration'); return { diff --git a/lambda/__tests__/__snapshots__/06.email-templates.test.ts.snap b/lambda/__tests__/__snapshots__/06.email-templates.test.ts.snap index f80df4c95..3adc134ad 100644 --- a/lambda/__tests__/__snapshots__/06.email-templates.test.ts.snap +++ b/lambda/__tests__/__snapshots__/06.email-templates.test.ts.snap @@ -974,7 +974,7 @@ exports[`Email template snapshots Should return the expected email for DELETE_IN "

Dear Pathfinder SSO friend,

The IDIR user with the idir username test-user has an inactive guid in our system. They have been removed from - client test-client and associated roles test-role + client test-client in the dev environment and associated roles test-role

This user was also a Team Admin for your integration, to help ensure a transition to the appropriate admin, please diff --git a/lambda/__tests__/helpers/common-mocks.js b/lambda/__tests__/helpers/common-mocks.js new file mode 100644 index 000000000..f7c4fce91 --- /dev/null +++ b/lambda/__tests__/helpers/common-mocks.js @@ -0,0 +1,15 @@ +jest.mock('../../app/src/authenticate'); + +jest.mock('../../app/src/utils/rate-limiters', () => { + return { + logsRateLimiter: jest.fn((req, res, next) => { + next(); + }), + }; +}); + +jest.mock('../../shared/utils/ches', () => { + return { + sendEmail: jest.fn(), + }; +}); diff --git a/lambda/__tests__/helpers/modules/integrations.ts b/lambda/__tests__/helpers/modules/integrations.ts index e0ede4362..4ecb2b612 100644 --- a/lambda/__tests__/helpers/modules/integrations.ts +++ b/lambda/__tests__/helpers/modules/integrations.ts @@ -73,8 +73,9 @@ export const createRequestQueueItem = async ( requestData: RequestData, action: QUEUE_ACTION, ageSeconds?: number, + attempts: number = 0, ) => { - const queueItem: any = { type: 'request', action, requestId, request: requestData }; + const queueItem: any = { type: 'request', action, requestId, request: requestData, attempts }; if (ageSeconds) { const currentTime = new Date(); const secondsAgoTime = currentTime.getTime() - ageSeconds * 1000; diff --git a/lambda/__tests__/jest.setup.js b/lambda/__tests__/jest.setup.js index cfc4d58f5..77c4e0ddb 100644 --- a/lambda/__tests__/jest.setup.js +++ b/lambda/__tests__/jest.setup.js @@ -1,5 +1,7 @@ const { createImportSpecifier } = require('typescript'); const { sequelize, models } = require('../shared/sequelize/models/models'); +require('./helpers/common-mocks'); + process.env.GH_SECRET = 'test'; process.env.API_AUTH_SECRET = 'test'; process.env.NODE_ENV = 'development'; diff --git a/lambda/app/package-lock.json b/lambda/app/package-lock.json index 2698efa8e..63ade22e5 100644 --- a/lambda/app/package-lock.json +++ b/lambda/app/package-lock.json @@ -10,16 +10,19 @@ "license": "Apache-2.0", "dependencies": { "@azure/msal-node": "^2.6.4", - "@keycloak/keycloak-admin-client": "18.0.1", + "@keycloak/keycloak-admin-client": "^24.0.5", "cors": "^2.8.5", "deep-diff": "^1.0.2", "express": "^4.21.0", + "express-rate-limit": "^7.4.1", "http-errors": "^2.0.0", + "ioredis": "^5.4.1", "json-schema": "^0.4.0", "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.5", "jws": "^4.0.0", "lambda-api-router": "^1.0.6", + "rate-limit-redis": "^4.2.0", "react": "^18.1.0", "react-jsonschema-form": "^1.8.1", "serverless-http": "^3.0.3", @@ -68,19 +71,24 @@ "node": ">=6.9.0" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@keycloak/keycloak-admin-client": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-18.0.1.tgz", - "integrity": "sha512-gmKjWbeLv5FvwlqDl+f6ZPFsgCOjFObYOPpmXj+v9itQd1PrSq6d1l1b7xIYyhAZnZ1pkKnDmzqtVrk+XrkAEg==", + "version": "24.0.5", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-24.0.5.tgz", + "integrity": "sha512-SXDVtQ3ov7GQbxXq51Uq8lzhwzQwNg6XiY50ZA9whuUe2t/0zPT4Zd/LcULcjweIjSNWWgfbDyN1E3yRSL8Qqw==", "license": "Apache-2.0", "dependencies": { - "axios": "^0.26.1", - "camelize-ts": "^1.0.8", - "keycloak-js": "^17.0.1", - "lodash": "^4.17.21", - "query-string": "^7.0.1", - "url-join": "^4.0.0", - "url-template": "^2.0.8" + "camelize-ts": "^3.0.0", + "url-join": "^5.0.0", + "url-template": "^3.1.1" + }, + "engines": { + "node": ">=18" } }, "node_modules/@types/body-parser": { @@ -242,35 +250,6 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", @@ -339,10 +318,22 @@ } }, "node_modules/camelize-ts": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-1.0.9.tgz", - "integrity": "sha512-ePOW3V2qrQ0qtRlcTM6Qe3nXremdydIwsMKI1Vl2NBGM0tOo8n2xzJ7YOQpV1GIKHhs3p+F40ThI8/DoYWbYKQ==", - "license": "MIT" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz", + "integrity": "sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/content-disposition": { "version": "0.5.4", @@ -411,15 +402,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/deep-diff": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", @@ -442,6 +424,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -570,6 +561,21 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", + "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -582,15 +588,6 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -608,26 +605,6 @@ "node": ">= 0.8" } }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -780,6 +757,47 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -789,12 +807,6 @@ "node": ">= 0.10" } }, - "node_modules/js-sha256": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", - "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", - "license": "MIT" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -887,16 +899,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keycloak-js": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-17.0.1.tgz", - "integrity": "sha512-mbLBSoogCBX5VYeKCdEz8BaRWVL9twzSqArRU3Mo3Z7vEO1mghGZJ5IzREfiMEi7kTUZtk5i9mu+Yc0koGkK6g==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.5.1", - "js-sha256": "^0.9.0" - } - }, "node_modules/lambda-api-router": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/lambda-api-router/-/lambda-api-router-1.0.6.tgz", @@ -920,12 +922,24 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -1164,24 +1178,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1190,6 +1186,18 @@ "node": ">= 0.6" } }, + "node_modules/rate-limit-redis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.0.tgz", + "integrity": "sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express-rate-limit": ">= 6" + } + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -1251,6 +1259,27 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "license": "MIT" }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.10", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", @@ -1402,14 +1431,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.1", @@ -1420,15 +1446,6 @@ "node": ">= 0.8" } }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1468,16 +1485,22 @@ } }, "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "license": "MIT" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } }, "node_modules/url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", - "license": "BSD" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", + "integrity": "sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA==", + "license": "BSD-3-Clause", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } }, "node_modules/utils-merge": { "version": "1.0.1", diff --git a/lambda/app/package.json b/lambda/app/package.json index bd512a4a5..6e1496e3e 100644 --- a/lambda/app/package.json +++ b/lambda/app/package.json @@ -18,16 +18,19 @@ }, "dependencies": { "@azure/msal-node": "^2.6.4", - "@keycloak/keycloak-admin-client": "18.0.1", + "@keycloak/keycloak-admin-client": "^24.0.5", "cors": "^2.8.5", "deep-diff": "^1.0.2", "express": "^4.21.0", + "express-rate-limit": "^7.4.1", "http-errors": "^2.0.0", + "ioredis": "^5.4.1", "json-schema": "^0.4.0", "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.5", "jws": "^4.0.0", "lambda-api-router": "^1.0.6", + "rate-limit-redis": "^4.2.0", "react": "^18.1.0", "react-jsonschema-form": "^1.8.1", "serverless-http": "^3.0.3", diff --git a/lambda/app/src/controllers/logs.ts b/lambda/app/src/controllers/logs.ts index ea02e38b0..6bb783d8c 100644 --- a/lambda/app/src/controllers/logs.ts +++ b/lambda/app/src/controllers/logs.ts @@ -10,20 +10,21 @@ const MAX_DAYS = 3; const allowedEnvs = ['dev', 'test', 'prod']; -export const fetchLogs = async (session: Session, env: string, id: number, start: string, end: string) => { - // Check user owns requested logs - const userRequest = await getAllowedRequest(session, id); - if (!userRequest) return { status: 401, message: "You are not authorized to view this integration's logs" }; - - const { clientId } = userRequest; - // Validate user supplied inputs - const hasRequiredQueryParams = start && end && env; +export const fetchLogs = async ( + env: string, + clientId: string, + requestId: string, + start: string, + end: string, + userId?: string, +) => { + const hasRequiredParams = start && end && env; - if (!hasRequiredQueryParams) { - return { status: 400, message: 'Not all query params sent. Please include start, end and env.' }; + if (!hasRequiredParams) { + return { status: 400, message: 'Not all parameters sent. Please include start, end and env.' }; } if (!allowedEnvs.includes(env)) { - return { status: 400, message: `The env query param must be one of ${allowedEnvs.join(', ')}.` }; + return { status: 400, message: `The env parameter must be one of ${allowedEnvs.join(', ')}.` }; } const unixStartTime = new Date(start).getTime(); @@ -38,13 +39,17 @@ export const fetchLogs = async (session: Session, env: string, id: number, start }; } + if (unixStartTime > unixEndTime) { + return { status: 400, message: `End date must be later than start date.` }; + } + if (unixEndTime - unixStartTime > MAX_DAYS * 60 * 60 * 24 * 1000) { return { status: 400, message: `Date range must be less ${MAX_DAYS} days.` }; } const eventMeta = { - requestId: userRequest.id, - idirUserid: session.idir_userid, + requestId, + idirUserid: userId, details: { environment: env, clientId, diff --git a/lambda/app/src/controllers/roles.ts b/lambda/app/src/controllers/roles.ts index cc70e652d..db6d46d44 100644 --- a/lambda/app/src/controllers/roles.ts +++ b/lambda/app/src/controllers/roles.ts @@ -11,7 +11,7 @@ import { getCompositeClientRoles, } from '../keycloak/users'; import { models } from '@lambda-shared/sequelize/models/models'; -import { destroyRequestRole, updateCompositeRoles } from '@lambda-app/queries/roles'; +import { destroyRequestRole, createCompositeRolesDB } from '@lambda-app/queries/roles'; import createHttpError from 'http-errors'; import { isAdmin } from '@lambda-app/utils/helpers'; import { Session } from '@lambda-shared/interfaces'; @@ -129,8 +129,7 @@ export const setCompositeRoles = async ( compositeRoleNames, }); - await updateCompositeRoles(result?.name, result?.composites, integration?.id, environment); - + await createCompositeRolesDB(result?.name, result?.composites, integration?.id, environment); return result; }; diff --git a/lambda/app/src/controllers/user.ts b/lambda/app/src/controllers/user.ts index 45aab4c06..ae14bf513 100644 --- a/lambda/app/src/controllers/user.ts +++ b/lambda/app/src/controllers/user.ts @@ -174,38 +174,48 @@ export const isAllowedToManageRoles = async (session: Session, integrationId: nu }; export const deleteStaleUsers = async ( - user: UserRepresentation & { clientData: { client: string; roles: string[] }[] }, + user: UserRepresentation & { clientData: { client: string; roles: string[] }[]; env: string }, ) => { try { + const deletedFromProduction = user.env === 'prod'; const userHadRoles = user?.clientData && user?.clientData?.length > 0; // Send formatted email with roles information to all team members if the deleted user had roles. if (userHadRoles) { - user.clientData.map(async (cl: { client: string; roles: string[] }) => { - const integration = await models.request.findOne({ - where: { - clientId: cl.client, - }, - raw: true, - }); - if (integration?.teamId) { - const userEmails = await getAllEmailsOfTeam(integration.teamId); - let isTeamAdmin = false; - userEmails.map((u: any) => { - if (u.idir_email === user.email && u.role === 'admin') { - isTeamAdmin = true; - } - }); - await sendTemplate(EMAILS.DELETE_INACTIVE_IDIR_USER, { - teamId: integration.teamId, - username: user.attributes.idir_username || user.username, - clientId: cl.client, - roles: cl.roles, - teamAdmin: isTeamAdmin, + await Promise.all( + user.clientData.map(async (cl: { client: string; roles: string[] }) => { + const integration = await models.request.findOne({ + where: { + clientId: cl.client, + }, + raw: true, }); - } - }); + if (integration?.teamId && !integration.archived) { + const userEmails = await getAllEmailsOfTeam(integration.teamId); + let isTeamAdmin = false; + // Only production users affect team management + if (deletedFromProduction) { + userEmails.map((u) => { + if (u.idir_email === user.email && u.role === 'admin') { + isTeamAdmin = true; + } + }); + } + await sendTemplate(EMAILS.DELETE_INACTIVE_IDIR_USER, { + teamId: integration.teamId, + username: user.attributes.idir_username || user.username, + clientId: cl.client, + roles: cl.roles, + env: user.env, + teamAdmin: isTeamAdmin, + }); + } + }), + ); } + // User management handling only applies to production user deletions. + if (!deletedFromProduction) return true; + if (!user.attributes.idir_user_guid) throw new createHttpError.BadRequest('user guid is required'); const existingUser = await models.user.findOne({ where: { idir_userid: user.attributes.idir_user_guid } }); @@ -254,13 +264,14 @@ export const deleteStaleUsers = async ( rqst.userId = ssoUser.id; await rqst.save(); // Notification was already sent above if roles were included. - if (!userHadRoles) { + if (!userHadRoles && !rqst.archived) { await sendTemplate(EMAILS.DELETE_INACTIVE_IDIR_USER, { teamId: rqst.teamId, username: user.attributes.idir_username || user.username, clientId: rqst.id, teamAdmin: team.role === 'admin', roles: [], + env: user.env, }); } } diff --git a/lambda/app/src/main.ts b/lambda/app/src/main.ts index ac2a27c0e..eeb4668a3 100644 --- a/lambda/app/src/main.ts +++ b/lambda/app/src/main.ts @@ -28,6 +28,7 @@ router.use( origin: process.env.LOCAL_DEV === 'true' ? '*' : 'https://bcgov.github.io', methods: ['OPTIONS', 'GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], + exposedHeaders: ['X-Message'], credentials: true, }), ); diff --git a/lambda/app/src/queries/roles.ts b/lambda/app/src/queries/roles.ts index 311103a72..99916a88c 100644 --- a/lambda/app/src/queries/roles.ts +++ b/lambda/app/src/queries/roles.ts @@ -14,21 +14,26 @@ export const getRolesWithEnvironments = async (integrationId: number) => { return results; }; -export const updateCompositeRoles = async ( +export const createCompositeRolesDB = async ( roleName: string, compositeRoleNames: string[], integrationId: number, environment: string, ) => { - const dbRole = await models.requestRole.findOne({ + let dbRole = await models.requestRole.findOne({ where: { name: roleName, requestId: integrationId, environment: environment, }, }); + if (!dbRole) { - throw new createHttpError.NotFound(`role ${roleName} not found`); + dbRole = await models.requestRole.create({ + name: roleName, + environment: environment, + requestId: integrationId, + }); } const dbCompositeRoles = await models.requestRole.findAll({ where: { @@ -40,16 +45,52 @@ export const updateCompositeRoles = async ( }, raw: true, }); - if (dbCompositeRoles.length > 0) { - dbRole.composite = true; - dbRole.compositeRoles = dbCompositeRoles.map((cr) => cr.id); - } else { - dbRole.composite = false; - dbRole.compositeRoles = []; + for (const compRole of compositeRoleNames) { + const compRoleId = dbCompositeRoles.find((cr) => cr.name === compRole).id; + if (!dbRole.compositeRoles.includes(compRoleId)) dbRole.compositeRoles = dbRole.compositeRoles.concat(compRoleId); + if (!dbRole.composite) dbRole.composite = true; } + return await dbRole.save(); }; +export const deleteCompositeRolesDB = async ( + roleName: string, + compositeRoleName: string, + integrationId: number, + environment: string, +) => { + const dbRole = await models.requestRole.findOne({ + where: { + name: roleName, + requestId: integrationId, + environment: environment, + }, + }); + + if (dbRole) { + const dbCompositeRole = await models.requestRole.findOne({ + where: { + name: compositeRoleName, + requestId: integrationId, + environment: environment, + }, + raw: true, + }); + + if (dbCompositeRole) { + const updatedComposites = dbRole.compositeRoles.filter((cr) => cr !== dbCompositeRole.id); + + dbRole.compositeRoles = updatedComposites; + + if (updatedComposites.length === 0) { + dbRole.composite = false; + } + return await dbRole.save(); + } + } +}; + export const getCompositeParentRoles = async (roleName: string, integrationId: number, environment: string) => { const [results] = await sequelize.query( 'select * from request_roles where (select id from request_roles where name = :roleName and environment = :environment and request_id = :integrationId) = ANY(composite_roles) and request_id = :integrationId and environment = :environment;', diff --git a/lambda/app/src/queries/team.ts b/lambda/app/src/queries/team.ts index f06ae24b4..4ec2a3a62 100644 --- a/lambda/app/src/queries/team.ts +++ b/lambda/app/src/queries/team.ts @@ -166,7 +166,7 @@ export const findAllowedTeamUsers = async (teamId: number, userId: number, optio }); }; -export const getAllEmailsOfTeam = async (teamId: number) => { +export const getAllEmailsOfTeam = async (teamId: number): Promise<{ idir_email: string; role: string }[]> => { const [userEmails] = await sequelize.query( 'SELECT a.idir_email, b.role FROM users a join users_teams b ON a.id = b.user_id AND b.team_id = :teamId', { diff --git a/lambda/app/src/routes.ts b/lambda/app/src/routes.ts index e8248222f..a8feabf75 100644 --- a/lambda/app/src/routes.ts +++ b/lambda/app/src/routes.ts @@ -51,7 +51,7 @@ import { Session, User } from '../../shared/interfaces'; import { inviteTeamMembers } from '../src/utils/helpers'; import { getAllowedTeam, getAllowedTeams } from '@lambda-app/queries/team'; import { parseInvitationToken } from '@lambda-app/helpers/token'; -import { findMyOrTeamIntegrationsByService } from '@lambda-app/queries/request'; +import { findMyOrTeamIntegrationsByService, getAllowedRequest } from '@lambda-app/queries/request'; import { isAdmin } from './utils/helpers'; import { createClientRole, @@ -75,6 +75,7 @@ import { EMAILS } from '@lambda-shared/enums'; import { fetchLogs, fetchMetrics } from '@lambda-app/controllers/logs'; import { getPrivacyZones, getAttributes } from './controllers/bc-services-card'; import createHttpError from 'http-errors'; +import { logsRateLimiter } from './utils/rate-limiters'; const APP_URL = process.env.APP_URL || ''; @@ -96,690 +97,706 @@ const handleError = (res, err) => { }; export const setRoutes = (app: any) => { - app.options(`/*`, async (req, res) => { - res.status(200).json(null); - }); - - app.get(`/heartbeat`, async (req, res) => { - try { - const result = await wakeUpAll(); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/github/discussions`, async (req, res) => { - try { - const result = await fetchDiscussions(); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/verify-token`, async (req, res) => { - try { + try { + app.options(`/*`, async (req, res) => { + res.status(200).json(null); + }); + + app.get(`/heartbeat`, async (req, res) => { + try { + const result = await wakeUpAll(); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/github/discussions`, async (req, res) => { + try { + const result = await fetchDiscussions(); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/verify-token`, async (req, res) => { + try { + const session = (await authenticate(req.headers)) as Session; + res.status(200).json(session); + return session; + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/teams/verify`, async (req, res) => { + try { + const token = req.query.token; + if (!token) return res.redirect(`${APP_URL}/verify-user?message=notoken`); + else { + const { error, message, userId, teamId } = parseInvitationToken(token); + + if (error) return res.redirect(`${APP_URL}/verify-user?message=${message}`); + + const verified = await verifyTeamMember(userId, teamId); + if (!verified) return res.redirect(`${APP_URL}/verify-user?message=notfound`); + + return res.redirect(`${APP_URL}/verify-user?message=success&teamId=${teamId}`); + } + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/delete-inactive-idir-users`, async (req, res) => { + try { + const { Authorization, authorization } = req.headers || {}; + const authHeader = Authorization || authorization; + if (!authHeader || authHeader !== process.env.API_AUTH_SECRET) { + res.status(401).json({ success: false, message: 'not authorized' }); + return false; + } + const result = await deleteStaleUsers(req.body); + if (result) res.status(200).json({ success: true }); + else res.status(404).json({ success: false, message: 'user not found' }); + } catch (err) { + handleError(res, err); + } + }); + + app.use(async (req, res, next) => { const session = (await authenticate(req.headers)) as Session; - res.status(200).json(session); - return session; - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/teams/verify`, async (req, res) => { - try { - const token = req.query.token; - if (!token) return res.redirect(`${APP_URL}/verify-user?message=notoken`); - else { - const { error, message, userId, teamId } = parseInvitationToken(token); - - if (error) return res.redirect(`${APP_URL}/verify-user?message=${message}`); - - const verified = await verifyTeamMember(userId, teamId); - if (!verified) return res.redirect(`${APP_URL}/verify-user?message=notfound`); - - return res.redirect(`${APP_URL}/verify-user?message=success&teamId=${teamId}`); - } - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/delete-inactive-idir-users`, async (req, res) => { - try { - const { Authorization, authorization } = req.headers || {}; - const authHeader = Authorization || authorization; - if (!authHeader || authHeader !== process.env.API_AUTH_SECRET) { + if (!session) { res.status(401).json({ success: false, message: 'not authorized' }); return false; } - const result = await deleteStaleUsers(req.body); - if (result) res.status(200).json({ success: true }); - else res.status(404).json({ success: false, message: 'user not found' }); - } catch (err) { - handleError(res, err); - } - }); - - app.use(async (req, res, next) => { - const session = (await authenticate(req.headers)) as Session; - if (!session) { - res.status(401).json({ success: false, message: 'not authorized' }); - return false; - } - - try { - const user: User = await findOrCreateUser(session); - user.isAdmin = isAdmin(session); - session.user = user; - req.user = user; - req.session = session; - } catch (err) { - handleError(res, err); - return false; - } - - if (next) next(); - }); - - app.get(`/me`, async (req, res) => { - try { - const integrations = await findMyOrTeamIntegrationsByService(req.user.id); - res.status(200).json({ ...req.user, integrations }); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/me`, async (req, res) => { - try { - const result = await updateProfile(req.session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post('/surveys', async (req, res) => { - try { - const { rating, message, triggerEvent } = req.body; - - if (!rating || !triggerEvent) { - return res.status(422).json({ message: 'Please include the keys "rating" and "triggerEvent" in the body.' }); - } - - // awaiting so email won't send if db save errors - await createSurvey(req.session, req.body); - await sendTemplate(EMAILS.SURVEY_COMPLETED, { user: req.session.user, rating, message, triggerEvent }); - - res.status(200).send(); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/requests-all`, async (req, res) => { - try { - const result = await getRequestAll(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/requests`, async (req, res) => { - try { - const { include } = req.query || {}; - const result = await getRequests(req.session as Session, req.user, include); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/team-integrations/:teamId`, async (req, res) => { - try { - const { teamId } = req.params; - const result = await getIntegrations(req.session as Session, teamId, req.user); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/requests`, async (req, res) => { - try { - const result = await createRequest(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.put(`/requests`, async (req, res) => { - try { - const { submit } = req.query || {}; - const result = await updateRequest(req.session as Session, req.body, req.user, submit); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/requests/:id/resubmit`, async (req, res) => { - try { - const { id } = req.params || {}; - if (!id) { - throw new createHttpError.NotFound('integration ID not found'); - } - const result = await resubmitRequest(req.session as Session, Number(id)); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/requests/:id/restore`, async (req, res) => { - try { - assertSessionRole(req.session, 'sso-admin'); - const { id } = req.params || {}; - let { email } = req.body || {}; - if (typeof email === 'string') { - email = email.toLowerCase(); - } - if (!id) { - throw new createHttpError.NotFound('integration ID not found'); - } - const result = await restoreRequest(req.session as Session, Number(id), email); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/requests/:id/logs`, async (req, res) => { - try { - const { id } = req.params || {}; - const { start, end, env } = req.query || {}; - const { status, message, data } = await fetchLogs(req.session, env, id, start, end); - if (status === 200) { - res.setHeader('Content-Length', JSON.stringify(data).length); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Content-Disposition', `attachment`); - res.status(status).send(data); - } else { - res.status(status).send({ message }); - } - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/requests/:id/metrics`, async (req, res) => { - try { - const { id } = req.params || {}; - const { fromDate, toDate, env } = req.query || {}; - const { status, message, data } = await fetchMetrics(req.session, id, env, fromDate, toDate); - if (status === 200) res.status(status).send(data); - else res.status(status).send({ message }); - } catch (err) { - handleError(res, err); - } - }); - - app.delete(`/requests`, async (req, res) => { - try { - const { id } = req.query || {}; - - const authorized = await isAllowedToDeleteIntegration(req.session as Session, id); - - if (!authorized) - return res.status(401).json({ success: false, message: 'You are not authorized to delete this integration' }); - - const result = await deleteRequest(req.session as Session, req.user, Number(id)); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/request`, async (req, res) => { - try { - const result = await getRequest(req.session as Session, req.user, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.put(`/request-metadata`, async (req, res) => { - try { - const result = await updateRequestMetadata(req.session as Session, req.user, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/installation`, async (req, res) => { - try { - const result = await getInstallation(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.put(`/installation`, async (req, res) => { - try { - const result = await changeSecret(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/users`, async (req, res) => { - try { - const result = await searchKeycloakUsers(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/roles`, async (req, res) => { - try { - const result = await listRoles(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/user-roles`, async (req, res) => { - try { - const result = await listClientRolesByUsers((req.session as Session).user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.put(`/keycloak/user-role`, async (req, res) => { - try { - const result = await updateUserRoleMapping((req.session as Session).user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.put(`/keycloak/user-roles`, async (req, res) => { - try { - const result = await updateUserRoleMappings((req.session as Session).user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/role-users`, async (req, res) => { - try { - const result = await listUsersByRole(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/set-composite-roles`, async (req, res) => { - try { - const authorized = await isAllowedToManageRoles(req.session as Session, req.body.integrationId); - - if (!authorized) - return res.status(401).json({ success: false, message: 'You are not authorized to update composite roles' }); - - const result = await setCompositeRoles((req.session as Session).user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/get-composite-roles`, async (req, res) => { - try { - const result = await listCompositeRoles(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/roles`, async (req, res) => { - try { - const result = await createClientRole((req.session as Session).user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/role`, async (req, res) => { - try { - const result = await getClientRole((req.session as Session).user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/bulk-roles`, async (req, res) => { - try { - const authorized = await isAllowedToManageRoles(req.session as Session, req.body.integrationId); - - if (!authorized) - return res.status(401).json({ success: false, message: 'You are not authorized to create role' }); - - const result = await bulkCreateClientRoles((req.session as Session).user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/keycloak/delete-role`, async (req, res) => { - try { - const authorized = await isAllowedToManageRoles(req.session as Session, req.body.integrationId); - - if (!authorized) - return res.status(401).json({ success: false, message: 'You are not authorized to delete role' }); - - const result = await deleteRoles((req.session as Session).user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/bceid-webservice/idir/search`, async (req, res) => { - try { - const result = await searchIdirUsers(req.body); - if (!result) res.status(404).send(); - else res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/bceid-webservice/idir/import`, async (req, res) => { - try { - const result = await importIdirUser(req.body); - if (!result) res.status(404).send(); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get('/idir-users', async (req, res) => { - try { - const { email } = req.query; - if (!email) { - res.status(400).send('Must include email query parameter'); - return; - } - const result = await searchIdirEmail(email); - res.status(200).send(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/events`, async (req, res) => { - try { - const result = await getEvents(req.session as Session, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/teams`, async (req, res) => { - try { - const result = await listTeams(req.user); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/allowed-teams`, async (req, res) => { - try { - const result = await getAllowedTeams(req.user, { raw: true }); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/allowed-teams/:id`, async (req, res) => { - try { - const { id } = req.params; - const result = await getAllowedTeam(id, req.user, { raw: true }); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/teams`, async (req, res) => { - try { - const result = await createTeam(req.user, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.put(`/teams/:id`, async (req, res) => { - try { - const { id } = req.params; - const result = await updateTeam(req.user, id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/teams/:id/members`, async (req, res) => { - try { - const { id } = req.params; - const result = await addUsersToTeam(id, req.user.id, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.put(`/teams/:id/members/:memberId`, async (req, res) => { - try { - const { id, memberId } = req.params; - const result = await updateMemberInTeam(req.user.id, id, memberId, req.body); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.delete(`/teams/:id/members/:memberId`, async (req, res) => { - try { - const { id, memberId } = req.params; - const result = await removeUserFromTeam(req.user.id, memberId, id); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/teams/:id/members`, async (req, res) => { - try { - const { id } = req.params; - const result = await findAllowedTeamUsers(id, req.user.id); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/teams/:id/invite`, async (req, res) => { - try { - const { id } = req.params; - await inviteTeamMembers(req.user.id, [req.body], id); - res.status(200).send(); - } catch (err) { - handleError(res, err); - } - }); - - app.delete(`/teams/:id`, async (req, res) => { - try { - const { id } = req.params; - const result = await deleteTeam(req.session as Session, id); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.post(`/teams/:id/service-accounts`, async (req, res) => { - try { - const { id: teamId } = req.params; - const result = await requestServiceAccount(req.session as Session, req.user.id, teamId, req.user.displayName); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/teams/:id/service-accounts`, async (req, res) => { - try { - const { id: teamId } = req.params; - const result = await getServiceAccounts(req.user.id, teamId); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/teams/:id/service-accounts/:saId`, async (req, res) => { - try { - const { id: teamId, saId } = req.params; - const result = await getServiceAccount(req.user.id, teamId, saId); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/teams/:id/service-accounts/:saId/restore`, async (req, res) => { - try { - const { id: teamId, saId } = req.params; - assertSessionRole(req.session, 'sso-admin'); - const result = await restoreTeamServiceAccount(req.session as Session, req.user.id, teamId, saId); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/teams/:id/service-accounts/:saId/credentials`, async (req, res) => { - try { - const { id: teamId, saId } = req.params; - const result = await getServiceAccountCredentials(req.user.id, teamId, saId); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.put(`/teams/:id/service-accounts/:saId/credentials`, async (req, res) => { - try { - const { id: teamId, saId } = req.params; - const result = await updateServiceAccountSecret(req.user.id, teamId, saId); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.delete(`/teams/:id/service-accounts/:saId`, async (req, res) => { - try { - const { id: teamId, saId } = req.params; - const result = await deleteServiceAccount(req.session as Session, req.user.id, teamId, saId); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/reports/all-standard-integrations`, async (req, res) => { - try { - assertSessionRole(req.session, 'sso-admin'); - const result = await getAllStandardIntegrations(); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/reports/database-tables`, async (req, res) => { - try { - assertSessionRole(req.session, 'sso-admin'); - const result = await getDatabaseTable(req.query.type, req.query.orderBy); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/reports/all-bceid-approved-requests-and-events`, async (req, res) => { - try { - assertSessionRole(req.session, 'sso-admin'); - const result = await getBceidApprovedRequestsAndEvents(); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get(`/reports/data-integrity`, async (req, res) => { - try { - assertSessionRole(req.session, 'sso-admin'); - const result = await getDataIntegrityReport(); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get('/bc-services-card/privacy-zones', async (req, res) => { - try { - const result = await getPrivacyZones(); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); - - app.get('/bc-services-card/claim-types', async (req, res) => { - try { - const result = await getAttributes(); - res.status(200).json(result); - } catch (err) { - handleError(res, err); - } - }); + + try { + const user: User = await findOrCreateUser(session); + user.isAdmin = isAdmin(session); + session.user = user; + req.user = user; + req.session = session; + } catch (err) { + handleError(res, err); + return false; + } + + if (next) next(); + }); + + app.get(`/me`, async (req, res) => { + try { + const integrations = await findMyOrTeamIntegrationsByService(req.user.id); + res.status(200).json({ ...req.user, integrations }); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/me`, async (req, res) => { + try { + const result = await updateProfile(req.session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post('/surveys', async (req, res) => { + try { + const { rating, message, triggerEvent } = req.body; + + if (!rating || !triggerEvent) { + return res.status(422).json({ message: 'Please include the keys "rating" and "triggerEvent" in the body.' }); + } + + // awaiting so email won't send if db save errors + await createSurvey(req.session, req.body); + await sendTemplate(EMAILS.SURVEY_COMPLETED, { user: req.session.user, rating, message, triggerEvent }); + + res.status(200).send(); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/requests-all`, async (req, res) => { + try { + const result = await getRequestAll(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/requests`, async (req, res) => { + try { + const { include } = req.query || {}; + const result = await getRequests(req.session as Session, req.user, include); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/team-integrations/:teamId`, async (req, res) => { + try { + const { teamId } = req.params; + const result = await getIntegrations(req.session as Session, teamId, req.user); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/requests`, async (req, res) => { + try { + const result = await createRequest(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.put(`/requests`, async (req, res) => { + try { + const { submit } = req.query || {}; + const result = await updateRequest(req.session as Session, req.body, req.user, submit); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/requests/:id/resubmit`, async (req, res) => { + try { + const { id } = req.params || {}; + if (!id) { + throw new createHttpError.NotFound('integration ID not found'); + } + const result = await resubmitRequest(req.session as Session, Number(id)); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/requests/:id/restore`, async (req, res) => { + try { + assertSessionRole(req.session, 'sso-admin'); + const { id } = req.params || {}; + let { email } = req.body || {}; + if (typeof email === 'string') { + email = email.toLowerCase(); + } + if (!id) { + throw new createHttpError.NotFound('integration ID not found'); + } + const result = await restoreRequest(req.session as Session, Number(id), email); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/requests/:id/logs`, logsRateLimiter, async (req, res) => { + try { + const { id } = req.params || {}; + const { start, end, env } = req.query || {}; + const userRequest = await getAllowedRequest(req.session, id); + if (!userRequest) { + return res.status(403).send('forbidden'); + } + const { status, message, data } = await fetchLogs( + env, + userRequest.clientId, + userRequest.id, + start, + end, + req.session.idir_userid, + ); + if (status === 200) { + res.setHeader('X-Message', message); + res.setHeader('Content-Length', JSON.stringify(data).length); + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Content-Disposition', `attachment`); + res.status(status).send(data); + } else { + res.status(status).send({ message }); + } + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/requests/:id/metrics`, async (req, res) => { + try { + const { id } = req.params || {}; + const { fromDate, toDate, env } = req.query || {}; + const { status, message, data } = await fetchMetrics(req.session, id, env, fromDate, toDate); + if (status === 200) res.status(status).send(data); + else res.status(status).send({ message }); + } catch (err) { + handleError(res, err); + } + }); + + app.delete(`/requests`, async (req, res) => { + try { + const { id } = req.query || {}; + + const authorized = await isAllowedToDeleteIntegration(req.session as Session, id); + + if (!authorized) + return res.status(401).json({ success: false, message: 'You are not authorized to delete this integration' }); + + const result = await deleteRequest(req.session as Session, req.user, Number(id)); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/request`, async (req, res) => { + try { + const result = await getRequest(req.session as Session, req.user, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.put(`/request-metadata`, async (req, res) => { + try { + const result = await updateRequestMetadata(req.session as Session, req.user, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/installation`, async (req, res) => { + try { + const result = await getInstallation(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.put(`/installation`, async (req, res) => { + try { + const result = await changeSecret(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/users`, async (req, res) => { + try { + const result = await searchKeycloakUsers(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/roles`, async (req, res) => { + try { + const result = await listRoles(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/user-roles`, async (req, res) => { + try { + const result = await listClientRolesByUsers((req.session as Session).user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.put(`/keycloak/user-role`, async (req, res) => { + try { + const result = await updateUserRoleMapping((req.session as Session).user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.put(`/keycloak/user-roles`, async (req, res) => { + try { + const result = await updateUserRoleMappings((req.session as Session).user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/role-users`, async (req, res) => { + try { + const result = await listUsersByRole(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/set-composite-roles`, async (req, res) => { + try { + const authorized = await isAllowedToManageRoles(req.session as Session, req.body.integrationId); + + if (!authorized) + return res.status(401).json({ success: false, message: 'You are not authorized to update composite roles' }); + + const result = await setCompositeRoles((req.session as Session).user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/get-composite-roles`, async (req, res) => { + try { + const result = await listCompositeRoles(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/roles`, async (req, res) => { + try { + const result = await createClientRole((req.session as Session).user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/role`, async (req, res) => { + try { + const result = await getClientRole((req.session as Session).user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/bulk-roles`, async (req, res) => { + try { + const authorized = await isAllowedToManageRoles(req.session as Session, req.body.integrationId); + + if (!authorized) + return res.status(401).json({ success: false, message: 'You are not authorized to create role' }); + + const result = await bulkCreateClientRoles((req.session as Session).user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/keycloak/delete-role`, async (req, res) => { + try { + const authorized = await isAllowedToManageRoles(req.session as Session, req.body.integrationId); + + if (!authorized) + return res.status(401).json({ success: false, message: 'You are not authorized to delete role' }); + + const result = await deleteRoles((req.session as Session).user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/bceid-webservice/idir/search`, async (req, res) => { + try { + const result = await searchIdirUsers(req.body); + if (!result) res.status(404).send(); + else res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/bceid-webservice/idir/import`, async (req, res) => { + try { + const result = await importIdirUser(req.body); + if (!result) res.status(404).send(); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get('/idir-users', async (req, res) => { + try { + const { email } = req.query; + if (!email) { + res.status(400).send('Must include email query parameter'); + return; + } + const result = await searchIdirEmail(email); + res.status(200).send(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/events`, async (req, res) => { + try { + const result = await getEvents(req.session as Session, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/teams`, async (req, res) => { + try { + const result = await listTeams(req.user); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/allowed-teams`, async (req, res) => { + try { + const result = await getAllowedTeams(req.user, { raw: true }); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/allowed-teams/:id`, async (req, res) => { + try { + const { id } = req.params; + const result = await getAllowedTeam(id, req.user, { raw: true }); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/teams`, async (req, res) => { + try { + const result = await createTeam(req.user, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.put(`/teams/:id`, async (req, res) => { + try { + const { id } = req.params; + const result = await updateTeam(req.user, id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/teams/:id/members`, async (req, res) => { + try { + const { id } = req.params; + const result = await addUsersToTeam(id, req.user.id, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.put(`/teams/:id/members/:memberId`, async (req, res) => { + try { + const { id, memberId } = req.params; + const result = await updateMemberInTeam(req.user.id, id, memberId, req.body); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.delete(`/teams/:id/members/:memberId`, async (req, res) => { + try { + const { id, memberId } = req.params; + const result = await removeUserFromTeam(req.user.id, memberId, id); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/teams/:id/members`, async (req, res) => { + try { + const { id } = req.params; + const result = await findAllowedTeamUsers(id, req.user.id); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/teams/:id/invite`, async (req, res) => { + try { + const { id } = req.params; + await inviteTeamMembers(req.user.id, [req.body], id); + res.status(200).send(); + } catch (err) { + handleError(res, err); + } + }); + + app.delete(`/teams/:id`, async (req, res) => { + try { + const { id } = req.params; + const result = await deleteTeam(req.session as Session, id); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.post(`/teams/:id/service-accounts`, async (req, res) => { + try { + const { id: teamId } = req.params; + const result = await requestServiceAccount(req.session as Session, req.user.id, teamId, req.user.displayName); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/teams/:id/service-accounts`, async (req, res) => { + try { + const { id: teamId } = req.params; + const result = await getServiceAccounts(req.user.id, teamId); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/teams/:id/service-accounts/:saId`, async (req, res) => { + try { + const { id: teamId, saId } = req.params; + const result = await getServiceAccount(req.user.id, teamId, saId); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/teams/:id/service-accounts/:saId/restore`, async (req, res) => { + try { + const { id: teamId, saId } = req.params; + assertSessionRole(req.session, 'sso-admin'); + const result = await restoreTeamServiceAccount(req.session as Session, req.user.id, teamId, saId); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/teams/:id/service-accounts/:saId/credentials`, async (req, res) => { + try { + const { id: teamId, saId } = req.params; + const result = await getServiceAccountCredentials(req.user.id, teamId, saId); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.put(`/teams/:id/service-accounts/:saId/credentials`, async (req, res) => { + try { + const { id: teamId, saId } = req.params; + const result = await updateServiceAccountSecret(req.user.id, teamId, saId); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.delete(`/teams/:id/service-accounts/:saId`, async (req, res) => { + try { + const { id: teamId, saId } = req.params; + const result = await deleteServiceAccount(req.session as Session, req.user.id, teamId, saId); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/reports/all-standard-integrations`, async (req, res) => { + try { + assertSessionRole(req.session, 'sso-admin'); + const result = await getAllStandardIntegrations(); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/reports/database-tables`, async (req, res) => { + try { + assertSessionRole(req.session, 'sso-admin'); + const result = await getDatabaseTable(req.query.type, req.query.orderBy); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/reports/all-bceid-approved-requests-and-events`, async (req, res) => { + try { + assertSessionRole(req.session, 'sso-admin'); + const result = await getBceidApprovedRequestsAndEvents(); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get(`/reports/data-integrity`, async (req, res) => { + try { + assertSessionRole(req.session, 'sso-admin'); + const result = await getDataIntegrityReport(); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get('/bc-services-card/privacy-zones', async (req, res) => { + try { + const result = await getPrivacyZones(); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + + app.get('/bc-services-card/claim-types', async (req, res) => { + try { + const result = await getAttributes(); + res.status(200).json(result); + } catch (err) { + handleError(res, err); + } + }); + } catch (err) { + console.error('Failed to initialize routes', err); + } }; diff --git a/lambda/app/src/utils/rate-limiters.ts b/lambda/app/src/utils/rate-limiters.ts new file mode 100644 index 000000000..ba00ab04b --- /dev/null +++ b/lambda/app/src/utils/rate-limiters.ts @@ -0,0 +1,26 @@ +import rateLimit from 'express-rate-limit'; +import RedisStore from 'rate-limit-redis'; +import RedisClient from 'ioredis'; + +const getClientIp = (req) => { + const id = req.params?.id ?? req.params?.integrationId; + const env = req.query?.env ?? req.params?.environment; + const clientIp = req.headers['X-Forwarded-For'] ?? req.connection.remoteAddress; + return `${id}-${env}-${clientIp}`; +}; + +export const logsRateLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hour + limit: 10, + standardHeaders: 'draft-7', + legacyHeaders: false, + keyGenerator: (req) => getClientIp(req), + message: 'Too many requests, please try again later.', + store: + process.env.NODE_ENV === 'production' + ? new RedisStore({ + // @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis + sendCommand: async (...args: string[]) => new RedisClient({ host: process.env.REDIS_HOST }).call(...args), + }) + : null, +}); diff --git a/lambda/app/yarn.lock b/lambda/app/yarn.lock index 966800f10..83d4a233b 100644 --- a/lambda/app/yarn.lock +++ b/lambda/app/yarn.lock @@ -24,18 +24,19 @@ core-js "^2.6.12" regenerator-runtime "^0.13.4" -"@keycloak/keycloak-admin-client@18.0.1": - version "18.0.1" - resolved "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-18.0.1.tgz" - integrity sha512-gmKjWbeLv5FvwlqDl+f6ZPFsgCOjFObYOPpmXj+v9itQd1PrSq6d1l1b7xIYyhAZnZ1pkKnDmzqtVrk+XrkAEg== - dependencies: - axios "^0.26.1" - camelize-ts "^1.0.8" - keycloak-js "^17.0.1" - lodash "^4.17.21" - query-string "^7.0.1" - url-join "^4.0.0" - url-template "^2.0.8" +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + +"@keycloak/keycloak-admin-client@^24.0.5": + version "24.0.5" + resolved "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-24.0.5.tgz" + integrity sha512-SXDVtQ3ov7GQbxXq51Uq8lzhwzQwNg6XiY50ZA9whuUe2t/0zPT4Zd/LcULcjweIjSNWWgfbDyN1E3yRSL8Qqw== + dependencies: + camelize-ts "^3.0.0" + url-join "^5.0.0" + url-template "^3.1.1" "@types/body-parser@*": version "1.19.2" @@ -151,18 +152,6 @@ asn1.js@^5.3.0: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" -axios@^0.26.1: - version "0.26.1" - resolved "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz" - integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== - dependencies: - follow-redirects "^1.14.8" - -base64-js@^1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - bn.js@^4.0.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" @@ -212,10 +201,15 @@ call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" -camelize-ts@^1.0.8: - version "1.0.9" - resolved "https://registry.npmjs.org/camelize-ts/-/camelize-ts-1.0.9.tgz" - integrity sha512-ePOW3V2qrQ0qtRlcTM6Qe3nXremdydIwsMKI1Vl2NBGM0tOo8n2xzJ7YOQpV1GIKHhs3p+F40ThI8/DoYWbYKQ== +camelize-ts@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz" + integrity sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ== + +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== content-disposition@0.5.4: version "0.5.4" @@ -252,6 +246,13 @@ cors@^2.8.5: object-assign "^4" vary "^1" +debug@^4.3.4: + version "4.3.7" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -259,11 +260,6 @@ debug@2.6.9: dependencies: ms "2.0.0" -decode-uri-component@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz" - integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== - deep-diff@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz" @@ -278,6 +274,11 @@ define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" @@ -345,7 +346,12 @@ etag@~1.8.1: resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -express@^4.21.0: +express-rate-limit@^7.4.1, "express-rate-limit@>= 6": + version "7.4.1" + resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz" + integrity sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg== + +express@^4.21.0, "express@4 || 5 || ^5.0.0-beta.1": version "4.21.0" resolved "https://registry.npmjs.org/express/-/express-4.21.0.tgz" integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== @@ -392,11 +398,6 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -filter-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz" - integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== - finalhandler@1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" @@ -410,11 +411,6 @@ finalhandler@1.3.1: statuses "2.0.1" unpipe "~1.0.0" -follow-redirects@^1.14.8: - version "1.15.9" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - forwarded@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" @@ -512,16 +508,26 @@ inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@2.0.4: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ioredis@^5.4.1: + version "5.4.1" + resolved "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz" + integrity sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -js-sha256@^0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== - "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -596,14 +602,6 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" -keycloak-js@^17.0.1: - version "17.0.1" - resolved "https://registry.npmjs.org/keycloak-js/-/keycloak-js-17.0.1.tgz" - integrity sha512-mbLBSoogCBX5VYeKCdEz8BaRWVL9twzSqArRU3Mo3Z7vEO1mghGZJ5IzREfiMEi7kTUZtk5i9mu+Yc0koGkK6g== - dependencies: - base64-js "^1.5.1" - js-sha256 "^0.9.0" - lambda-api-router@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/lambda-api-router/-/lambda-api-router-1.0.6.tgz" @@ -613,11 +611,21 @@ lambda-api-router@^1.0.6: minimist "^1.2.5" path-to-regexp "^6.2.0" +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" @@ -648,7 +656,7 @@ lodash.once@^4.0.0: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.17.15, lodash@^4.17.20: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -707,7 +715,7 @@ minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -ms@^2.1.1, ms@2.1.3: +ms@^2.1.1, ms@^2.1.3, ms@2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -788,21 +796,16 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" -query-string@^7.0.1: - version "7.1.3" - resolved "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz" - integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== - dependencies: - decode-uri-component "^0.2.2" - filter-obj "^1.1.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +rate-limit-redis@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.0.tgz" + integrity sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA== + raw-body@2.5.2: version "2.5.2" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" @@ -844,6 +847,18 @@ react@^18.1.0, react@>=15: dependencies: loose-envify "^1.1.0" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + regenerator-runtime@^0.13.4: version "0.13.10" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz" @@ -937,21 +952,16 @@ side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" -split-on-first@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz" - integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -strict-uri-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" - integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== - toidentifier@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" @@ -977,15 +987,15 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url-join@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz" - integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== +url-join@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz" + integrity sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA== -url-template@^2.0.8: - version "2.0.8" - resolved "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz" - integrity sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw== +url-template@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz" + integrity sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA== utils-merge@1.0.1: version "1.0.1" diff --git a/lambda/css-api/k6-tests/cleanup.sh b/lambda/css-api/k6-tests/cleanup.sh index cb622979a..6090c2552 100755 --- a/lambda/css-api/k6-tests/cleanup.sh +++ b/lambda/css-api/k6-tests/cleanup.sh @@ -1,6 +1,6 @@ #!/bin/bash -ACCESS_TOKEN=$(curl -X POST -d grant_type=client_credentials -d client_id=$K6_CLIENT_ID -d client_secret=$K6_CLIENT_SECRET https://sso-keycloak-6-b861c7-test.apps.silver.devops.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token | jq '.access_token' -r) +ACCESS_TOKEN=$(curl -X POST -d grant_type=client_credentials -d client_id=$K6_CLIENT_ID -d client_secret=$K6_CLIENT_SECRET $K6_KEYCLOAK_TOKEN_URL | jq '.access_token' -r) INTEGRATION_ID=$(curl -X GET $K6_CSS_API_URL/integrations -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.data | .[].id' -r) diff --git a/lambda/css-api/k6-tests/load-tests.js b/lambda/css-api/k6-tests/load-tests.js index fe3c71129..650d67170 100644 --- a/lambda/css-api/k6-tests/load-tests.js +++ b/lambda/css-api/k6-tests/load-tests.js @@ -19,12 +19,7 @@ let integrationId; http.setResponseCallback(http.expectedStatuses({ min: 200, max: 204 })); export const options = { - stages: [ - { duration: '10m', target: 100 }, // simulate ramp-up of traffic from 1 to 50 users over 3 minutes. - ], - thresholds: { - 'http_req_duration{status:504}': ['max=0'], - }, + stages: [{ duration: '5m', target: 100 }], }; export function setup() { diff --git a/lambda/css-api/k6-tests/smoke-tests/modules/user-role-mappings.js b/lambda/css-api/k6-tests/smoke-tests/modules/user-role-mappings.js index 2bf3db004..e3dea24f5 100644 --- a/lambda/css-api/k6-tests/smoke-tests/modules/user-role-mappings.js +++ b/lambda/css-api/k6-tests/smoke-tests/modules/user-role-mappings.js @@ -403,7 +403,7 @@ export function testUserRoleMapping(options) { console.debug(`Response from CSS API: ${JSON.stringify(response, 0, 2)}`); check(response, { - 'should return 400 when user with invalid or no idp passed': (r) => r.status === 400, + 'should return 404 when user with invalid or no idp passed': (r) => r.status === 404, }); sleep(SLEEP_DURATION); @@ -511,7 +511,7 @@ export function testUserRoleMapping(options) { console.debug(`Response from CSS API: ${JSON.stringify(response, 0, 2)}`); check(response, { - 'should return 400 when invalid or no idp passed': (r) => r.status === 400, + 'should return 404 when invalid or no idp passed': (r) => r.status === 404, }); sleep(SLEEP_DURATION); @@ -570,7 +570,7 @@ export function testUserRoleMapping(options) { console.debug(`Response from CSS API: ${JSON.stringify(response, 0, 2)}`); check(response, { - 'should return 400 when non-associated role name passed': (r) => r.status === 400, + 'should return 404 when non-associated role name passed': (r) => r.status === 404, }); sleep(SLEEP_DURATION); @@ -585,7 +585,7 @@ export function testUserRoleMapping(options) { console.debug(`Response from CSS API: ${JSON.stringify(response, 0, 2)}`); check(response, { - 'should return 400 when username with invalid or no idp passed': (r) => r.status === 400, + 'should return 404 when username with invalid or no idp passed': (r) => r.status === 404, }); sleep(SLEEP_DURATION); diff --git a/lambda/css-api/package-lock.json b/lambda/css-api/package-lock.json index d90823ee7..564014cfe 100644 --- a/lambda/css-api/package-lock.json +++ b/lambda/css-api/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { + "@keycloak/keycloak-admin-client": "^26.0.6", "cors": "^2.8.5", "express": "^4.21.0", "http-errors": "^2.0.0", @@ -29,6 +30,20 @@ "@types/jws": "^3.2.3" } }, + "node_modules/@keycloak/keycloak-admin-client": { + "version": "26.0.6", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-26.0.6.tgz", + "integrity": "sha512-pZmaSAyg+LwQ3qnZF+01ZkURpcoEdLAloUK5KOZjE9jyNd86EHdx98/XmTYaJIuQ6ydMXxTWWc5Grq18H+PvJQ==", + "license": "Apache-2.0", + "dependencies": { + "camelize-ts": "^3.0.0", + "url-join": "^5.0.0", + "url-template": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@types/http-errors": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", @@ -196,6 +211,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelize-ts": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz", + "integrity": "sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1204,6 +1228,24 @@ "node": ">= 0.8" } }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/url-template": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", + "integrity": "sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA==", + "license": "BSD-3-Clause", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1230,6 +1272,16 @@ } }, "dependencies": { + "@keycloak/keycloak-admin-client": { + "version": "26.0.6", + "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-26.0.6.tgz", + "integrity": "sha512-pZmaSAyg+LwQ3qnZF+01ZkURpcoEdLAloUK5KOZjE9jyNd86EHdx98/XmTYaJIuQ6ydMXxTWWc5Grq18H+PvJQ==", + "requires": { + "camelize-ts": "^3.0.0", + "url-join": "^5.0.0", + "url-template": "^3.1.1" + } + }, "@types/http-errors": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", @@ -1361,6 +1413,11 @@ "set-function-length": "^1.2.1" } }, + "camelize-ts": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz", + "integrity": "sha512-cgRwKKavoDKLTjO4FQTs3dRBePZp/2Y9Xpud0FhuCOTE86M2cniKN4CCXgRnsyXNMmQMifVHcv6SPaMtTx6ofQ==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2096,6 +2153,16 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==" + }, + "url-template": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", + "integrity": "sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/lambda/css-api/src/authenticate.ts b/lambda/css-api/src/authenticate.ts index dd5fee656..419617dd5 100644 --- a/lambda/css-api/src/authenticate.ts +++ b/lambda/css-api/src/authenticate.ts @@ -71,7 +71,7 @@ const validateJWTSignature = async (token) => { return { success: true, data: { teamId: team }, err: null }; } catch (err) { - console.log(err); + console.error(err); if (err.name === 'TokenExpiredError') failedAuth.err = 'token expired'; else if (err.name === 'JsonWebTokenError') failedAuth.err = 'invalid token'; diff --git a/lambda/css-api/src/controllers/integration-controller.ts b/lambda/css-api/src/controllers/integration-controller.ts index d1d6bdfab..c22a0414d 100644 --- a/lambda/css-api/src/controllers/integration-controller.ts +++ b/lambda/css-api/src/controllers/integration-controller.ts @@ -1,11 +1,11 @@ -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import { IntegrationService } from '../services/integration-service'; @injectable() export class IntegrationController { private attributes = ['id', 'projectName', 'authType', 'environments', 'status', 'createdAt', 'updatedAt']; - constructor(private integrationService: IntegrationService) {} + constructor(@inject('IntegrationService') private integrationService: IntegrationService) {} public async getIntegration(id: number, teamId: number) { const int = await this.integrationService.getById(id, teamId); diff --git a/lambda/css-api/src/controllers/role-controller.ts b/lambda/css-api/src/controllers/role-controller.ts index 2f0872b82..f1b93372d 100644 --- a/lambda/css-api/src/controllers/role-controller.ts +++ b/lambda/css-api/src/controllers/role-controller.ts @@ -1,10 +1,10 @@ -import { injectable } from 'tsyringe'; +import { container, inject, injectable } from 'tsyringe'; import { RoleService } from '../services/role-service'; import { RolePayload } from '../types'; @injectable() export class RoleController { - constructor(private roleService: RoleService) {} + constructor(@inject('RoleService') private roleService: RoleService) {} public async get(teamId: number, integrationId: number, environment: string, roleName: string) { return await this.roleService.getByName(teamId, integrationId, environment, roleName); diff --git a/lambda/css-api/src/controllers/user-controller.ts b/lambda/css-api/src/controllers/user-controller.ts index eeeae1c08..a176ddcfa 100644 --- a/lambda/css-api/src/controllers/user-controller.ts +++ b/lambda/css-api/src/controllers/user-controller.ts @@ -1,10 +1,10 @@ import { UserService } from '../services/user-service'; -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import { ListUsersFilterQuery } from '../types'; @injectable() export class UserController { - constructor(private userService: UserService) {} + constructor(@inject('UserService') private userService: UserService) {} public async listUsers(environment: string, idp: string, query: ListUsersFilterQuery) { return await this.userService.getUsers(environment, idp, query); diff --git a/lambda/css-api/src/controllers/user-role-mapping-controller.ts b/lambda/css-api/src/controllers/user-role-mapping-controller.ts index 2e82258b4..aedeec957 100644 --- a/lambda/css-api/src/controllers/user-role-mapping-controller.ts +++ b/lambda/css-api/src/controllers/user-role-mapping-controller.ts @@ -1,17 +1,13 @@ -import { findUserByRealm } from '@lambda-app/keycloak/users'; import { getValidator, postValidator, getUsersByRolenameValidator } from '../schemas/user-role-mapping'; -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import { UserRoleMappingService } from '../services/user-role-mapping-service'; import { parseErrors } from '../util'; -import { RoleService } from '../services/role-service'; import createHttpError from 'http-errors'; -import { updateUserProps } from '../helpers/users'; -import { updateRoleProps } from '../helpers/roles'; import { ListUserRoleMappingQuery, RolePayload, UserRoleMappingPayload, ListUsersByRoleName } from '../types'; @injectable() export class UserRoleMappingController { - constructor(private userRoleMappingService: UserRoleMappingService, private roleService: RoleService) {} + constructor(@inject('UserRoleMappingService') private userRoleMappingService: UserRoleMappingService) {} public async list(teamId: number, integrationId: number, environment: string, query: ListUserRoleMappingQuery) { const valid = getValidator(query || {}); diff --git a/lambda/css-api/src/routes.ts b/lambda/css-api/src/routes.ts index 067f8c7db..bdedac31f 100644 --- a/lambda/css-api/src/routes.ts +++ b/lambda/css-api/src/routes.ts @@ -10,6 +10,14 @@ import { TokenController } from './controllers/token-controller'; import { isEmpty } from 'lodash'; import createHttpError from 'http-errors'; import { UserController } from './controllers/user-controller'; +import { getIntegrationByIdAndTeam } from '@lambda-app/queries/request'; +import { fetchLogs } from '@lambda-app/controllers/logs'; +import { logsRateLimiter } from '@lambda-app/utils/rate-limiters'; +import { KeycloakService, KeycloakServiceFactory } from './services/keycloak-service'; +import { RoleService } from './services/role-service'; +import { IntegrationService } from './services/integration-service'; +import { UserRoleMappingService } from './services/user-role-mapping-service'; +import { UserService } from './services/user-service'; const tryJSON = (str: string) => { try { @@ -27,6 +35,14 @@ const handleError = (res, err) => { res.status(err.status || 422).json({ message }); }; +container.registerSingleton('KeycloakServiceFactory', KeycloakServiceFactory); +container.registerSingleton('DevKeycloakService', KeycloakService); +container.registerSingleton('TestKeycloakService', KeycloakService); +container.registerSingleton('ProdKeycloakService', KeycloakService); +container.registerSingleton('RoleService', RoleService); +container.registerSingleton('IntegrationService', IntegrationService); +container.registerSingleton('UserRoleMappingService', UserRoleMappingService); +container.registerSingleton('UserService', UserService); const integrationController = container.resolve(IntegrationController); const roleController = container.resolve(RoleController); const userRoleMappingController = container.resolve(UserRoleMappingController); @@ -140,6 +156,74 @@ export const setRoutes = (app: any) => { } }); + app.get(`/integrations/:integrationId/:environment/logs`, logsRateLimiter, async (req, res) => { + /*#swagger.auto = false + #swagger.tags = ['Logs'] + #swagger.path = '/integrations/{integrationId}/{environment}/logs' + #swagger.method = 'get' + #swagger.description = 'Get logs for the integration of the target environment' + #swagger.summary = 'Get logs for the integration and environment' + #swagger.parameters['integrationId'] = { + in: 'path', + description: 'Integration Id', + required: true, + type: 'number', + example: 1234 + } + #swagger.parameters['environment'] = { + in: 'path', + description: 'Environment', + required: true, + schema: { $ref: '#/components/schemas/environment' } + } + #swagger.parameters['start'] = { + in: 'query', + required: true, + description: 'Start Datetime in ISO 8601 format, RFC 2822 format, or milliseconds since epoch.', + example: '2024-11-14T10:00:00Z' + } + #swagger.parameters['end'] = { + in: 'query', + required: true, + description: 'End Datetime in ISO 8601 format, RFC 2822 format, or milliseconds since epoch.', + example: '2024-11-14T11:00:00Z' + } + #swagger.responses[200] = { + description: 'OK', + schema: { $ref: '#/components/schemas/logsResponse' } + } + #swagger.responses[400] = { + description: 'Bad Request', + schema: { message: 'string' } + } + #swagger.responses[403] = { + description: 'Forbidden', + schema: { message: 'string' } + } + #swagger.responses[429] = { + description: 'Too Many Requests. Will be rate limited after 10 requests for the same integration and environment per hour.', + schema: { message: 'string' } + } + #swagger.responses[500,504] = { + description: 'Server Error', + schema: { message: 'string' } + } + */ + try { + const { integrationId, environment } = req.params; + const { start, end } = req.query || {}; + const int = await getIntegrationByIdAndTeam(integrationId, req.teamId); + if (!int) { + return res.status(403).json({ message: 'forbidden' }); + } + const { status, message, data } = await fetchLogs(environment, int.clientId, int.id, start, end); + if (status === 200) res.status(status).send({ data, message }); + else res.status(status).send({ message }); + } catch (err) { + handleError(res, err); + } + }); + app.get(`/integrations/:integrationId/:environment/roles`, async (req, res) => { /*#swagger.auto = false #swagger.tags = ['Roles'] @@ -1249,7 +1333,7 @@ export const setRoutes = (app: any) => { #swagger.tags = ['Role-Mapping'] #swagger.path = '/integrations/{integrationId}/{environment}/users/{username}/roles' #swagger.method = 'get' - #swagger.description = 'Get roles associated with user for the integration of the target environment' + #swagger.description = 'Get roles associated with user for the integration of the target environment. For a service account, use the client ID as the username.' #swagger.summary = 'Get roles associated with user' #swagger.parameters['integrationId'] = { in: 'path', @@ -1375,7 +1459,7 @@ export const setRoutes = (app: any) => { #swagger.tags = ['Role-Mapping'] #swagger.path = '/integrations/{integrationId}/{environment}/users/{username}/roles' #swagger.method = 'post' - #swagger.description = 'Assign roles to a user for the integration of the target environment' + #swagger.description = 'Assign roles to a user for the integration of the target environment. For a service account, use the client ID as the username.' #swagger.summary = 'Assign roles to a user' #swagger.parameters['integrationId'] = { in: 'path', @@ -1443,7 +1527,7 @@ export const setRoutes = (app: any) => { #swagger.tags = ['Role-Mapping'] #swagger.path = '/integrations/{integrationId}/{environment}/users/{username}/roles/{roleName}' #swagger.method = 'delete' - #swagger.description = 'Unassign role from a user for the integration of the target environment' + #swagger.description = 'Unassign role from a user for the integration of the target environment. For a service account, use your client ID as the username.' #swagger.summary = 'Unassign role from a user' #swagger.parameters['integrationId'] = { in: 'path', diff --git a/lambda/css-api/src/services/keycloak-service.ts b/lambda/css-api/src/services/keycloak-service.ts new file mode 100644 index 000000000..3f5cc4556 --- /dev/null +++ b/lambda/css-api/src/services/keycloak-service.ts @@ -0,0 +1,422 @@ +import axios, { AxiosError, AxiosInstance } from 'axios'; +import { getKeycloakCredentials } from '../util'; +import jwt from 'jsonwebtoken'; +import { RolePayload } from '../types'; +import createHttpError from 'http-errors'; +import { inject, singleton } from 'tsyringe'; + +interface KeycloakTokenResponse { + access_token: string; + refresh_token: string; + expires_in: number; + refresh_expires_in: number; + token_type: string; + 'not-before-policy': number; + session_state: string; + scope: string; +} + +export class KeycloakService { + private realm: string = 'standard'; + private httpClient: AxiosInstance; + private environment: string; + private accessToken: string | null = null; + private refreshToken: string | null = null; + private refreshing: boolean = false; + + constructor() {} + + setEnvironment(environment: string) { + this.environment = environment; + const { keycloakUrl } = getKeycloakCredentials(environment); + this.httpClient = axios.create({ + baseURL: `${keycloakUrl}`, + }); + this.httpClient.interceptors.response.use( + (response) => { + return response; + }, + (error: AxiosError) => { + console.error('keycloak request path: ', error?.request?.path); + console.error('keycloak request headers: ', error?.request?.headers); + console.error('keycloak response status: ', error?.response?.status); + console.error('keycloak response data: ', error?.response?.data); + console.error('keycloak response status message: ', error?.response?.statusText); + throw error; + }, + ); + } + + getEnvironment() { + return this.environment; + } + + isTokenValid(accessToken: string) { + return (jwt.decode(accessToken) as any).exp > Date.now().valueOf() / 1000 + 30; + } + + async getAccessToken() { + if (this.accessToken && this.isTokenValid(this.accessToken)) { + return this.accessToken; + } + if ( + this.accessToken && + !this.isTokenValid(this.accessToken) && + this.refreshToken && + this.isTokenValid(this.refreshToken) + ) { + // refreshing access token 30 seconds earlier + if (this.refreshing) { + return this.accessToken; + } + this.refreshing = true; + const response = await this.httpClient.post( + '/auth/realms/master/protocol/openid-connect/token', + new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.refreshToken, + client_id: 'admin-cli', + }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + this.accessToken = response.data.access_token; + this.refreshToken = response.data.refresh_token; + this.refreshing = false; + return this.accessToken; + } else { + const { keycloakUsername, keycloakPassword } = getKeycloakCredentials(this.environment); + const response = await this.httpClient.post( + '/auth/realms/master/protocol/openid-connect/token', + new URLSearchParams({ + grant_type: 'password', + username: keycloakUsername, + password: keycloakPassword, + client_id: 'admin-cli', + }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + this.accessToken = response.data.access_token; + this.refreshToken = response.data.refresh_token; + return this.accessToken; + } + } + + async getClient(clientId: string) { + const accessToken = await this.getAccessToken(); + const response = await this.httpClient.get(`/auth/admin/realms/${this.realm}/clients?clientId=${clientId}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (response.data.length === 0) throw new createHttpError.NotFound(`client ${clientId} not found`); + return response.data[0]; + } + + async getUser(username: string) { + const accessToken = await this.getAccessToken(); + const response = await this.httpClient.get(`/auth/admin/realms/${this.realm}/users?username=${username}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (response.data.length === 0) throw new createHttpError.NotFound(`user ${username} not found`); + return response.data[0]; + } + + async createClientRole(clientId: string, role: RolePayload) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + try { + await this.httpClient.post(`/auth/admin/realms/${this.realm}/clients/${client?.id}/roles`, role, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + return await this.getClientRole(clientId, role.name); + } catch (error) { + if (error instanceof AxiosError && error.response) { + if (error.response.status === 409) { + throw new createHttpError.Conflict(`role ${role.name} already exists`); + } + } + } + } + + async deleteClientRole(clientId: string, roleName: string) { + try { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + await this.httpClient.delete(`/auth/admin/realms/${this.realm}/clients/${client?.id}/roles/${roleName}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + if (error instanceof AxiosError && error.response) { + if (error.response.status === 404) { + throw new createHttpError.NotFound(`role ${roleName} not found`); + } + } + } + } + + async getClientRole(clientId: string, roleName: string) { + try { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + const response = await this.httpClient.get( + `/auth/admin/realms/${this.realm}/clients/${client?.id}/roles/${roleName}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }, + ); + return response.data; + } catch (error) { + if (error instanceof AxiosError && error.response) { + if (error.response.status === 404) { + throw new createHttpError.NotFound(`role ${roleName} not found`); + } + } + } + } + + async listClientRoles(clientId: string) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + const response = await this.httpClient.get(`/auth/admin/realms/${this.realm}/clients/${client?.id}/roles`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + return response.data; + } + + async updateClientRole(clientId: string, roleName: string, role: RolePayload) { + try { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + await this.httpClient.put(`/auth/admin/realms/${this.realm}/clients/${client?.id}/roles/${roleName}`, role, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + return await this.getClientRole(clientId, role?.name); + } catch (error) { + if (error instanceof AxiosError && error.response) { + if (error.response.status === 404) { + throw new createHttpError.NotFound(`role ${roleName} not found`); + } else if (error.response.status === 409) { + throw new createHttpError.Conflict(`role ${role?.name} already exists`); + } + } + } + } + + async createCompositeRole(clientId: string, roleName: string, compositeRoles: RolePayload[]) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + const clientRoles = await this.listClientRoles(clientId); + const clientRolesNames = clientRoles.map((role) => role.name); + const rolesNames = compositeRoles.map((role) => role.name); + const invalidRoles = rolesNames.filter((role) => !clientRolesNames.includes(role)); + if (!clientRolesNames.includes(roleName)) { + throw new createHttpError.NotFound(`role ${roleName} not found`); + } + if (invalidRoles.length > 0) { + throw new createHttpError[404](`composite roles (${invalidRoles.join(', ')}) not found`); + } + await this.httpClient.post( + `/auth/admin/realms/${this.realm}/clients/${client?.id}/roles/${roleName}/composites`, + clientRoles.filter((r) => compositeRoles.find((role) => role.name === r.name)), + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }, + ); + return await this.getClientRole(clientId, roleName); + } + + async deleteCompositeRole(clientId: string, roleName: string, compositeRoleName: string) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + const clientRoles = await this.listClientRoles(clientId); + const clientRolesNames = clientRoles.map((role) => role.name); + + if (!clientRolesNames.includes(roleName)) { + throw new createHttpError.NotFound(`role ${roleName} not found`); + } else if (!clientRolesNames.includes(compositeRoleName)) { + throw new createHttpError.NotFound(`composite role ${compositeRoleName} not found`); + } + + await this.httpClient.delete( + `/auth/admin/realms/${this.realm}/clients/${client?.id}/roles/${roleName}/composites`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + data: clientRoles.filter((r) => r.name === compositeRoleName), + }, + ); + return await this.getClientRole(clientId, roleName); + } + + async getCompositeRoles(clientId: string, roleName: string, compositeRoleName?: string) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + const clientRoles = await this.listClientRoles(clientId); + const clientRolesNames = clientRoles.map((role) => role.name); + + if (!clientRolesNames.includes(roleName)) { + throw new createHttpError.NotFound(`role ${roleName} not found`); + } + if (compositeRoleName && !clientRolesNames.includes(compositeRoleName)) { + throw new createHttpError.NotFound(`composite role ${compositeRoleName} not found`); + } + + const response = await this.httpClient.get( + `/auth/admin/realms/${this.realm}/clients/${client?.id}/roles/${roleName}/composites`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }, + ); + if (compositeRoleName) { + const compositeRole = response.data.find((role) => role.name === compositeRoleName); + if (!compositeRole) { + throw new createHttpError.NotFound(`role ${compositeRoleName} not found`); + } + return compositeRole; + } + return response.data; + } + + async listUsersByClientRole(clientId: string, roleName: string, first: number, max: number) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + try { + const response = await this.httpClient.get( + `/auth/admin/realms/${this.realm}/clients/${client?.id}/roles/${roleName}/users?first=${first}&max=${max}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }, + ); + return response.data; + } catch (error) { + if (error instanceof AxiosError && error.response) { + if (error.response.status === 404) { + throw new createHttpError.NotFound(`role ${roleName} not found`); + } + } + } + } + + async listClientUserRoleMappings(clientId: string, username: string) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + const user = await this.getUser(username); + const response = await this.httpClient.get( + `/auth/admin/realms/${this.realm}/users/${user?.id}/role-mappings/clients/${client?.id}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }, + ); + return response.data; + } + + async addClientUserRoleMapping(clientId: string, username: string, roles: RolePayload[]) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + const user = await this.getUser(username); + + const clientRoles = await this.listClientRoles(clientId); + const clientRolesNames = clientRoles.map((role) => role.name); + const rolesNames = roles.map((role) => role.name); + const invalidRoles = rolesNames.filter((role) => !clientRolesNames.includes(role)); + if (invalidRoles.length > 0) { + throw new createHttpError[404](`roles (${invalidRoles.join(', ')}) not found`); + } + + await this.httpClient.post( + `/auth/admin/realms/${this.realm}/users/${user?.id}/role-mappings/clients/${client?.id}`, + clientRoles.filter((r) => roles.find((role) => role.name === r.name)), + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }, + ); + return await this.listClientUserRoleMappings(clientId, username); + } + + async deleteClientUserRoleMapping(clientId: string, username: string, roleName: string) { + const accessToken = await this.getAccessToken(); + const client = await this.getClient(clientId); + const user = await this.getUser(username); + const clientRoles = await this.listClientRoles(clientId); + const clientRolesNames = clientRoles.map((role) => role.name); + if (!clientRolesNames.includes(roleName)) { + throw new createHttpError.NotFound(`role ${roleName} not found`); + } + await this.httpClient.delete( + `/auth/admin/realms/${this.realm}/users/${user?.id}/role-mappings/clients/${client?.id}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + data: [clientRoles.find((role) => role.name === roleName)], + }, + ); + } +} + +@singleton() +export class KeycloakServiceFactory { + constructor( + @inject('DevKeycloakService') private devKeycloakService: KeycloakService, + @inject('TestKeycloakService') private testKeycloakService: KeycloakService, + @inject('ProdKeycloakService') private prodKeycloakService: KeycloakService, + ) {} + getKeycloakService(environment: string) { + switch (environment) { + case 'dev': + this.devKeycloakService.setEnvironment('dev'); + return this.devKeycloakService; + case 'test': + this.testKeycloakService.setEnvironment('test'); + return this.testKeycloakService; + case 'prod': + this.prodKeycloakService.setEnvironment('prod'); + return this.prodKeycloakService; + default: + throw new createHttpError[404](`environment ${environment} not found`); + } + } +} diff --git a/lambda/css-api/src/services/role-service.ts b/lambda/css-api/src/services/role-service.ts index 058655684..65fc4fbad 100644 --- a/lambda/css-api/src/services/role-service.ts +++ b/lambda/css-api/src/services/role-service.ts @@ -1,14 +1,5 @@ -import { - createRole, - deleteRole, - listClientRoles, - updateRole, - findClientRole, - manageRoleComposites, - getRoleComposites, - setCompositeClientRoles, -} from '@lambda-app/keycloak/users'; -import { injectable } from 'tsyringe'; +import { listClientRoles, findClientRole, getRoleComposites } from '@lambda-app/keycloak/users'; +import { container, inject, injectable, singleton } from 'tsyringe'; import { IntegrationService } from './integration-service'; import createHttpError from 'http-errors'; import { roleValidator, listOfrolesValidator } from '../schemas/role'; @@ -17,11 +8,13 @@ import { Role, RolePayload } from '../types'; import { Integration } from 'app/interfaces/Request'; import { parseErrors } from '../util'; import { models } from '@lambda-shared/sequelize/models/models'; -import { destroyRequestRole, updateCompositeRoles } from '@lambda-app/queries/roles'; +import { destroyRequestRole, deleteCompositeRolesDB, createCompositeRolesDB } from '@lambda-app/queries/roles'; +import { KeycloakServiceFactory } from './keycloak-service'; -@injectable() +@singleton() export class RoleService { - constructor(private integrationService: IntegrationService) {} + keycloakServiceFactory = container.resolve(KeycloakServiceFactory); + constructor(@inject('IntegrationService') private integrationService: IntegrationService) {} public async getAllByEnvironment(teamId: number, integrationId: number, environment: string) { const int = await this.integrationService.getById(integrationId, teamId); @@ -39,7 +32,8 @@ export class RoleService { public async createRole(teamId: number, integrationId: number, role: RolePayload, environment: string) { this.validateRole(role); const int = await this.integrationService.getById(integrationId, teamId); - const kcRole = await createRole(int, { environment, integrationId, roleName: role.name }); + const KeycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + const kcRole = await KeycloakService.createClientRole(int.clientId, role); if (kcRole) { await models.requestRole.create({ name: role?.name, @@ -47,19 +41,14 @@ export class RoleService { requestId: integrationId, }); } - return getAllowedRoleProps((await findClientRole(int, { environment, roleName: role?.name })) as Role); + return getAllowedRoleProps(kcRole); } public async deleteRole(teamId: number, integrationId: number, roleName: string, environment: string) { const int = await this.integrationService.getById(integrationId, teamId); - await deleteRole(int, { environment, integrationId, roleName }); - - const deletedRole = await findClientRole(int, { environment, roleName }); - - if (!deletedRole) { - await destroyRequestRole(int?.id, roleName, environment); - } - return; + const KeycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + await KeycloakService.deleteClientRole(int.clientId, roleName); + await destroyRequestRole(int?.id, roleName, environment); } public async updateRole( @@ -71,23 +60,30 @@ export class RoleService { ) { this.validateRole(role); const int = await this.integrationService.getById(integrationId, teamId); - const dbRole = await models.requestRole.findOne({ - where: { - name: roleName, - environment: environment, - requestId: integrationId, - }, - }); - if (!dbRole) throw new createHttpError[404](`role ${roleName} not found`); - await updateRole(int, { environment, integrationId, roleName, newRoleName: role.name }); - const updatedRole = await findClientRole(int, { environment, roleName: role.name }); - + const KeycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + const updatedRole = await KeycloakService.updateClientRole(int.clientId, roleName, role); if (updatedRole) { - dbRole.name = role?.name; - dbRole.environment = environment; - await dbRole.save(); + const dbRole = await models.requestRole.findOne({ + where: { + name: roleName, + environment: environment, + requestId: integrationId, + }, + }); + // if role exists in db then update else create + if (dbRole) { + dbRole.name = role?.name; + dbRole.environment = environment; + await dbRole.save(); + } else { + await models.requestRole.create({ + name: role?.name, + environment: environment, + requestId: integrationId, + }); + } } - return getAllowedRoleProps(updatedRole as Role); + return getAllowedRoleProps(updatedRole); } public validateRole(role: RolePayload) { @@ -109,52 +105,36 @@ export class RoleService { environment: string, compositeRoles: RolePayload[], ) { - let rolesToAdd = []; - const int = await this.integrationService.getById(integrationId, teamId); - - const existingRoles = await listClientRoles(int, { environment, integrationId }); - const role = existingRoles.find((role) => role.name === roleName); - if (!role) throw new createHttpError[404](`role ${roleName} not found`); const valid = listOfrolesValidator(compositeRoles); if (!valid) throw new createHttpError[400](parseErrors(listOfrolesValidator.errors)); + + const int = await this.integrationService.getById(integrationId, teamId); + + const KeycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + for (let role of compositeRoles) { if (role.name.trim().length === 0) throw new createHttpError[400]('invalid role'); if (role.name === roleName) throw new createHttpError[400](`role ${roleName} cannot be associated with itself`); - if (!existingRoles.find((existingRole) => existingRole.name === role.name)) - throw new createHttpError[404](`role ${role.name} not found`); - rolesToAdd.push(existingRoles.find((existingRole) => role.name === existingRole.name)); } - try { - const existingComposites = await getRoleComposites(int, environment, role.id); - // merge two arrays and remove duplicates - const compositeRolesToAdd = existingComposites - .map((r) => r.name) - .concat(rolesToAdd.map((r) => r.name)) - .filter((r, i, a) => a.indexOf(r) === i); - - const result = await setCompositeClientRoles(int, { - environment, - roleName, - compositeRoleNames: compositeRolesToAdd, - }); + const updatedRole = await KeycloakService.createCompositeRole(int.clientId, roleName, compositeRoles); - await updateCompositeRoles(result?.name, result?.composites, int?.id, environment); - } catch (err) { - console.error(err); - throw new createHttpError[500]('error creating composite roles'); + if (updatedRole) { + await createCompositeRolesDB( + roleName, + compositeRoles.map((r) => r.name), + int?.id, + environment, + ); } - return getAllowedRoleProps((await findClientRole(int, { environment, roleName })) as Role); + return getAllowedRoleProps(updatedRole); } public async getCompositeRoles(teamId: number, integrationId: number, roleName: string, environment: string) { const int = await this.integrationService.getById(integrationId, teamId); - const existingRoles = await listClientRoles(int, { environment, integrationId }); - const role = existingRoles.find((role) => role.name === roleName); - - if (!role) throw new createHttpError[404](`role ${roleName} not found`); - - return updateRoleProps((await getRoleComposites(int, environment, role.id)) as Role[]); + const keycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + const compositeRole = await keycloakService.getCompositeRoles(int.clientId, roleName); + return updateRoleProps(compositeRole); } public async getCompositeRole( @@ -165,23 +145,15 @@ export class RoleService { compositeRoleName: string, ) { const int = await this.integrationService.getById(integrationId, teamId); - const existingRoles = await listClientRoles(int, { environment, integrationId }); - const role = existingRoles.find((role) => role.name === roleName); - const compositeRole = existingRoles.find((role) => role.name === compositeRoleName); if (roleName === compositeRoleName) throw new createHttpError[400](`role name and composite role name cannot be same`); - if (!role) throw new createHttpError[404](`role ${roleName} not found`); - - if (!compositeRole) throw new createHttpError[404](`role ${compositeRoleName} not found`); + const keycloakService = this.keycloakServiceFactory.getKeycloakService(environment); - const compRoles = await getRoleComposites(int, environment, role.id); + const compositeRole = await keycloakService.getCompositeRoles(int.clientId, roleName, compositeRoleName); - if (!compRoles.find((role) => role.name === compositeRoleName)) - throw new createHttpError[404](`role ${compositeRoleName} is not associated with ${roleName}`); - - return getAllowedRoleProps(compRoles.find((role) => role.name === compositeRoleName) as Role); + return getAllowedRoleProps(compositeRole); } public async deleteCompositeRole( @@ -192,33 +164,12 @@ export class RoleService { compositeRoleName: string, ) { const int = await this.integrationService.getById(integrationId, teamId); - const existingRoles = await listClientRoles(int, { environment, integrationId }); - const role = existingRoles.find((role) => role.name === roleName); - const compositeRole = existingRoles.find((role) => role.name === compositeRoleName); - - if (!role) throw new createHttpError[404](`role ${roleName} not found`); - - if (!compositeRole) throw new createHttpError[404](`role ${compositeRoleName} not found`); + const keycloakService = this.keycloakServiceFactory.getKeycloakService(environment); if (roleName === compositeRoleName) throw new createHttpError[400](`role name and composite role name cannot be same`); - const compRoles = await getRoleComposites(int, environment, role.id); - - if (!compRoles.find((role) => role.name === compositeRoleName)) - throw new createHttpError[404](`role ${compositeRoleName} is not associated with ${roleName}`); - - try { - const result = await setCompositeClientRoles(int, { - environment, - roleName, - compositeRoleNames: compRoles.filter((r) => r.name !== compositeRoleName).map((r) => r.name), - }); - - await updateCompositeRoles(result?.name, result?.composites, int?.id, environment); - } catch (err) { - console.error(err); - throw new createHttpError[500]('error deleting composite roles'); - } + await keycloakService.deleteCompositeRole(int.clientId, roleName, compositeRoleName); + await deleteCompositeRolesDB(roleName, compositeRoleName, int?.id, environment); } } diff --git a/lambda/css-api/src/services/user-role-mapping-service.ts b/lambda/css-api/src/services/user-role-mapping-service.ts index c04765040..5cb30daf8 100644 --- a/lambda/css-api/src/services/user-role-mapping-service.ts +++ b/lambda/css-api/src/services/user-role-mapping-service.ts @@ -7,7 +7,7 @@ import { manageUserRole, manageUserRoles, } from '@lambda-app/keycloak/users'; -import { injectable } from 'tsyringe'; +import { container, inject, injectable } from 'tsyringe'; import { ListUserRoleMappingQuery, Role, RolePayload, User, UserRoleMappingPayload } from '../types'; import { RoleService } from './role-service'; import createHttpError from 'http-errors'; @@ -17,10 +17,27 @@ import { Integration } from 'app/interfaces/Request'; import includes from 'lodash.includes'; import { listOfrolesValidator } from '../schemas/role'; import { parseErrors } from '../util'; +import { KeycloakServiceFactory } from './keycloak-service'; @injectable() export class UserRoleMappingService { - constructor(private integrationService: IntegrationService, private roleService: RoleService) {} + keycloakServiceFactory = container.resolve(KeycloakServiceFactory); + constructor( + @inject('IntegrationService') private integrationService: IntegrationService, + @inject('RoleService') private roleService: RoleService, + ) {} + + /** + * Check if the username is the client ID and return that client's service account username if it matches. + * Internally keycloak creates a service account user with a name in the format service-account- + */ + private parseUsername(clientId, username) { + let parsedUsername = username; + if (clientId === username) { + parsedUsername = `service-account-${clientId}`; + } + return parsedUsername; + } public async getAllByQuery( teamId: number, @@ -79,7 +96,9 @@ export class UserRoleMappingService { public async getAllRolesByUser(teamId: number, integrationId: number, environment: string, username: string) { const int = await this.integrationService.getById(integrationId, teamId); - return await listUserRoles(int, { environment, username }); + const parsedUsername = this.parseUsername(int.clientId, username); + const keycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + return await keycloakService.listClientUserRoleMappings(int.clientId, parsedUsername); } public async manageRoleMapping( @@ -121,10 +140,10 @@ export class UserRoleMappingService { } public async listRolesByUsername(teamId: number, integrationId: number, environment: string, username: string) { - const user = await findUserByRealm(environment, username); - if (!user) throw new createHttpError[404](`user ${username} not found`); - const roles = await this.getAllRolesByUser(teamId, integrationId, environment, username); - return { data: updateRoleProps(roles as Role[]) }; + const int = await this.integrationService.getById(integrationId, teamId); + const keycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + const parsedUsername = this.parseUsername(int.clientId, username); + return { data: updateRoleProps(await keycloakService.listClientUserRoleMappings(int.clientId, parsedUsername)) }; } public async listUsersByRolename( @@ -136,7 +155,15 @@ export class UserRoleMappingService { max: number = 50, ) { const first = page > 1 ? max * (page - 1) : 0; - const userList = await this.getAllUsersByRole(teamId, integrationId, environment, roleName, first, max); + const keycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + const userList = await keycloakService.listUsersByClientRole( + ( + await this.integrationService.getById(integrationId, teamId) + ).clientId, + roleName, + first, + max, + ); return { page, data: updateUserProps(userList as User[]) }; } @@ -150,12 +177,15 @@ export class UserRoleMappingService { const valid = listOfrolesValidator(roles); if (!valid) throw new createHttpError[400](parseErrors(listOfrolesValidator.errors)); const int = await this.integrationService.getById(integrationId, teamId); - + const parsedUsername = this.parseUsername(int.clientId, username); + const keycloakService = this.keycloakServiceFactory.getKeycloakService(environment); for (let role of roles) { this.roleService.validateRole(role); - await manageUserRole(int, { environment, username, roleName: role.name, mode: 'add' }); } - return await this.listRolesByUsername(teamId, integrationId, environment, username); + + return { + data: updateRoleProps(await keycloakService.addClientUserRoleMapping(int.clientId, parsedUsername, roles)), + }; } public async deleteRoleFromUser( @@ -167,9 +197,8 @@ export class UserRoleMappingService { ) { this.roleService.validateRole({ name: roleName }); const int = await this.integrationService.getById(integrationId, teamId); - const existingUserRoles = await this.getAllRolesByUser(teamId, integrationId, environment, username); - if (!existingUserRoles.find((existingRole) => existingRole.name === roleName)) - throw new createHttpError[400](`role ${roleName} is not associated with user ${username}`); - await manageUserRole(int, { environment, username, roleName, mode: 'del' }); + const parsedUsername = this.parseUsername(int.clientId, username); + const keycloakService = this.keycloakServiceFactory.getKeycloakService(environment); + await keycloakService.deleteClientUserRoleMapping(int.clientId, parsedUsername, roleName); } } diff --git a/lambda/css-api/src/services/user-service.ts b/lambda/css-api/src/services/user-service.ts index 8bc4a510b..74861e272 100644 --- a/lambda/css-api/src/services/user-service.ts +++ b/lambda/css-api/src/services/user-service.ts @@ -1,4 +1,3 @@ -import { ListUsersFilterQuery } from '../types'; import { injectable } from 'tsyringe'; import { searchUsersByIdp } from '@lambda-app/keycloak/users'; import { diff --git a/lambda/css-api/src/swagger.js b/lambda/css-api/src/swagger.js index 673339ede..6c0bb2896 100644 --- a/lambda/css-api/src/swagger.js +++ b/lambda/css-api/src/swagger.js @@ -78,6 +78,15 @@ const doc = { name: 'client-role', composite: false, }, + logsResponse: { + data: [ + { + '@timestamp': 'string', + message: 'string', + }, + ], + message: 'string', + }, compositeRoleRequest: { $name: 'composite-role', }, diff --git a/lambda/css-api/src/types.ts b/lambda/css-api/src/types.ts index 0da5f4abf..7285b4140 100644 --- a/lambda/css-api/src/types.ts +++ b/lambda/css-api/src/types.ts @@ -5,6 +5,7 @@ export type UserRoleMappingPayload = { }; export type RolePayload = { + id?: string; name: string; }; diff --git a/lambda/css-api/src/util.ts b/lambda/css-api/src/util.ts index 6a35efd9f..ef670c3a1 100644 --- a/lambda/css-api/src/util.ts +++ b/lambda/css-api/src/util.ts @@ -1,14 +1,43 @@ import { models } from '@lambda-shared/sequelize/models/models'; +import createHttpError from 'http-errors'; import validator from 'validator'; export const createEvent = async (data: any) => { try { await models.event.create(data); } catch (err) { - console.log(err); + console.error(err); } }; export const parseErrors = (validationErrors) => { return validationErrors[0].message; }; + +export const getKeycloakCredentials = (environment: string) => { + let keycloakUrl; + let keycloakUsername; + let keycloakPassword; + + if (environment === 'dev') { + keycloakUrl = process.env.KEYCLOAK_V2_DEV_URL; + keycloakUsername = process.env.KEYCLOAK_V2_DEV_USERNAME; + keycloakPassword = process.env.KEYCLOAK_V2_DEV_PASSWORD; + } else if (environment === 'test') { + keycloakUrl = process.env.KEYCLOAK_V2_TEST_URL; + keycloakUsername = process.env.KEYCLOAK_V2_TEST_USERNAME; + keycloakPassword = process.env.KEYCLOAK_V2_TEST_PASSWORD; + } else if (environment === 'prod') { + keycloakUrl = process.env.KEYCLOAK_V2_PROD_URL; + keycloakUsername = process.env.KEYCLOAK_V2_PROD_USERNAME; + keycloakPassword = process.env.KEYCLOAK_V2_PROD_PASSWORD; + } else { + throw new createHttpError.BadRequest('invalid environment'); + } + + return { + keycloakUrl, + keycloakUsername, + keycloakPassword, + }; +}; diff --git a/lambda/css-api/yarn.lock b/lambda/css-api/yarn.lock index c97c27c3a..04359098b 100644 --- a/lambda/css-api/yarn.lock +++ b/lambda/css-api/yarn.lock @@ -4,38 +4,36 @@ "@types/http-errors@^1.8.2": version "1.8.2" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz" integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w== "@types/jsonwebtoken@^8.5.2": - version "8.5.9" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz#2c064ecb0b3128d837d2764aa0b117b0ff6e4586" - integrity sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg== + version "8.5.8" + resolved "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz" + integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== dependencies: "@types/node" "*" "@types/jwk-to-pem@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/jwk-to-pem/-/jwk-to-pem-2.0.3.tgz#ba06bc681c194473e9d8cdb85a835f2dd839fd92" - integrity sha512-I/WFyFgk5GrNbkpmt14auGO3yFK1Wt4jXzkLuI+fDBNtO5ZI2rbymyGd6bKzfSBEuyRdM64ZUwxU1+eDcPSOEQ== + version "2.0.1" + resolved "https://registry.npmjs.org/@types/jwk-to-pem/-/jwk-to-pem-2.0.1.tgz" + integrity sha512-QXmRPhR/LPzvXBHTPfG2BBfMTkNLUD7NyRcPft8m5xFCeANa1BZyLgT0Gw+OxdWx6i1WCpT27EqyggP4UUHMrA== "@types/jws@^3.2.3": - version "3.2.10" - resolved "https://registry.yarnpkg.com/@types/jws/-/jws-3.2.10.tgz#627054edcfc350978f270a3d505ce381c6910db9" - integrity sha512-cOevhttJmssERB88/+XvZXvsq5m9JLKZNUiGfgjUb5lcPRdV2ZQciU6dU76D/qXXFYpSqkP3PrSg4hMTiafTZw== + version "3.2.4" + resolved "https://registry.npmjs.org/@types/jws/-/jws-3.2.4.tgz" + integrity sha512-aqtH4dPw1wUjFZaeMD1ak/pf8iXlu/odFe+trJrvw0g1sTh93i+SCykg0Ek8C6B7rVK3oBORbfZAsKO7P10etg== dependencies: "@types/node" "*" "@types/node@*": - version "22.7.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" - integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== - dependencies: - undici-types "~6.19.2" + version "18.6.5" + resolved "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz" + integrity sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw== accepts@~1.3.8: version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: mime-types "~2.1.34" @@ -43,17 +41,17 @@ accepts@~1.3.8: acorn@^7.4.1: version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== array-flatten@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== asn1.js@^5.3.0: version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + resolved "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz" integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== dependencies: bn.js "^4.0.0" @@ -63,17 +61,17 @@ asn1.js@^5.3.0: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== bn.js@^4.0.0, bn.js@^4.11.9: version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== body-parser@1.20.3: version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" @@ -91,7 +89,7 @@ body-parser@1.20.3: brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" @@ -99,22 +97,22 @@ brace-expansion@^1.1.7: brorand@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== buffer-equal-constant-time@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== bytes@3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== call-bind@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: es-define-property "^1.0.0" @@ -125,34 +123,34 @@ call-bind@^1.0.7: concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== content-disposition@0.5.4: version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: safe-buffer "5.2.1" content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== cookie-signature@1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== cors@^2.8.5: version "2.8.5" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== dependencies: object-assign "^4" @@ -160,19 +158,19 @@ cors@^2.8.5: debug@2.6.9: version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" deepmerge@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== define-data-property@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: es-define-property "^1.0.0" @@ -181,30 +179,30 @@ define-data-property@^1.1.4: depd@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== destroy@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== ecdsa-sig-formatter@1.0.11: version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== dependencies: safe-buffer "^5.0.1" ee-first@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -elliptic@^6.5.7: - version "6.5.7" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" - integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== +elliptic@^6.5.4: + version "6.5.4" + resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== dependencies: bn.js "^4.11.9" brorand "^1.1.0" @@ -216,47 +214,47 @@ elliptic@^6.5.7: encodeurl@~1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== encodeurl@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== es-define-property@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz" integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== dependencies: get-intrinsic "^1.2.4" es-errors@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== escape-html@~1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== etag@~1.8.1: version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== express@^4.21.0: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + version "4.21.0" + resolved "https://registry.npmjs.org/express/-/express-4.21.0.tgz" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== dependencies: accepts "~1.3.8" array-flatten "1.1.1" body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.7.1" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -285,7 +283,7 @@ express@^4.21.0: finalhandler@1.3.1: version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" @@ -298,27 +296,27 @@ finalhandler@1.3.1: forwarded@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fresh@0.5.2: version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: es-errors "^1.3.0" @@ -329,7 +327,7 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: glob@^7.1.7: version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -341,31 +339,31 @@ glob@^7.1.7: gopd@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== dependencies: get-intrinsic "^1.1.3" has-property-descriptors@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: es-define-property "^1.0.0" has-proto@^1.0.1: version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz" integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== dependencies: inherits "^2.0.3" @@ -373,14 +371,14 @@ hash.js@^1.0.0, hash.js@^1.0.3: hasown@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" hmac-drbg@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz" integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== dependencies: hash.js "^1.0.3" @@ -389,7 +387,7 @@ hmac-drbg@^1.0.1: http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: depd "2.0.0" @@ -400,14 +398,14 @@ http-errors@2.0.0, http-errors@^2.0.0: iconv-lite@0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" @@ -415,22 +413,22 @@ inflight@^1.0.4: inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== json5@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonwebtoken@^9.0.2: version "9.0.2" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== dependencies: jws "^3.2.2" @@ -446,7 +444,7 @@ jsonwebtoken@^9.0.2: jwa@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz" integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== dependencies: buffer-equal-constant-time "1.0.1" @@ -455,7 +453,7 @@ jwa@^1.4.1: jwa@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz" integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== dependencies: buffer-equal-constant-time "1.0.1" @@ -463,17 +461,17 @@ jwa@^2.0.0: safe-buffer "^5.0.1" jwk-to-pem@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/jwk-to-pem/-/jwk-to-pem-2.0.6.tgz#0810c03307e873d5c81faeb650408fa3ae91eb9c" - integrity sha512-zPC/5vjyR08TpknpTGW6Z3V3lDf9dU92oHbf0jJlG8tGOzslF9xk2UiO/seSx2llCUrNAe+AvmuGTICSXiYU7A== + version "2.0.5" + resolved "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz" + integrity sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A== dependencies: asn1.js "^5.3.0" - elliptic "^6.5.7" + elliptic "^6.5.4" safe-buffer "^5.0.1" jws@^3.2.2: version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== dependencies: jwa "^1.4.1" @@ -481,7 +479,7 @@ jws@^3.2.2: jws@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + resolved "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz" integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== dependencies: jwa "^2.0.0" @@ -489,7 +487,7 @@ jws@^4.0.0: lambda-api-router@^1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/lambda-api-router/-/lambda-api-router-1.0.6.tgz#93367c696fac34ffcbe0623bca2f864293f3f691" + resolved "https://registry.npmjs.org/lambda-api-router/-/lambda-api-router-1.0.6.tgz" integrity sha512-5YBjBVuGtKbIa8y2bvEaWWYJCmOIowvjzd7rpTGlHWX6STTGWSresVdhLs+xgsG0Cr/TymyXhPd0wkEiFtcV2Q== dependencies: lodash "^4.17.20" @@ -498,89 +496,89 @@ lambda-api-router@^1.0.6: lodash.includes@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== lodash.isboolean@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== lodash.isinteger@^4.0.4: version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== lodash.isnumber@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== lodash.isplainobject@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== lodash.isstring@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== lodash.once@^4.0.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== lodash@^4.17.20: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== media-typer@0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== merge-descriptors@1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== methods@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== mime-db@1.52.0: version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" mime@1.6.0: version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== minimalistic-crypto-utils@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== minimatch@^3.1.1: version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -592,66 +590,66 @@ minimist@>=1.2.6, minimist@^1.2.5: ms@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.3, ms@^2.1.1: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== negotiator@0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== object-assign@^4: version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.13.1: version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== on-finished@2.4.1: version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: ee-first "1.1.1" once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" parseurl@~1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-to-regexp@0.1.10: version "0.1.10" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-to-regexp@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" - integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + version "6.2.1" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== proxy-addr@~2.0.7: version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: forwarded "0.2.0" @@ -659,19 +657,19 @@ proxy-addr@~2.0.7: qs@6.13.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: side-channel "^1.0.6" range-parser@~1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== raw-body@2.5.2: version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" @@ -680,28 +678,28 @@ raw-body@2.5.2: unpipe "1.0.0" reflect-metadata@^0.1.13: - version "0.1.14" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" - integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== + version "0.1.13" + resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== safe-buffer@5.2.1, safe-buffer@^5.0.1: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.5.4: version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== send@0.19.0: version "0.19.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" @@ -720,7 +718,7 @@ send@0.19.0: serve-static@1.16.2: version "1.16.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: encodeurl "~2.0.0" @@ -729,13 +727,13 @@ serve-static@1.16.2: send "0.19.0" serverless-http@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/serverless-http/-/serverless-http-3.2.0.tgz#68acc7735f7c876733c04f038ee20f31f5ebfc9b" - integrity sha512-QvSyZXljRLIGqwcJ4xsKJXwkZnAVkse1OajepxfjkBXV0BMvRS5R546Z4kCBI8IygDzkQY0foNPC/rnipaE9pQ== + version "3.1.0" + resolved "https://registry.npmjs.org/serverless-http/-/serverless-http-3.1.0.tgz" + integrity sha512-CwWO34otWjwJ3+meUlFoEhCOiEEpGDT/xnaZ835iMwF0PEL2GoMJMBRed6tUXYtrLAj4tTgkF6wiUbaJQyaEpw== set-function-length@^1.2.1: version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: define-data-property "^1.1.4" @@ -747,12 +745,12 @@ set-function-length@^1.2.1: setprototypeof@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== side-channel@^1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: call-bind "^1.0.7" @@ -762,13 +760,13 @@ side-channel@^1.0.6: statuses@2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== swagger-autogen@^2.23.1: - version "2.23.7" - resolved "https://registry.yarnpkg.com/swagger-autogen/-/swagger-autogen-2.23.7.tgz#40023e583b1d4b4321313bb92cc768488758f135" - integrity sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ== + version "2.23.1" + resolved "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.1.tgz" + integrity sha512-tOAb5cOGNPduIHKoOxndCRy2Mrg7xV3O1RerrWExrDxeSTjXhA350pyJd7VUDY6ZO9gbZ34Bjlc5CXkleUgvAQ== dependencies: acorn "^7.4.1" deepmerge "^4.2.2" @@ -776,56 +774,51 @@ swagger-autogen@^2.23.1: json5 "^2.2.3" swagger-ui-dist@^4.14.0: - version "4.19.1" - resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.19.1.tgz#c6d53ba0961a006c6b6b4323ba700e87f31da856" - integrity sha512-n/gFn+R7G/BXWwl5UZLw6F1YgWOlf3zkwGlsPhTMhNtAAolBGKg0JS5b2RKt5NI6/hSopVaSrki2wTIMUDDy2w== + version "4.14.0" + resolved "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.14.0.tgz" + integrity sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw== toidentifier@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== tslib@^1.9.3: version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tsyringe@^4.7.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.8.0.tgz#d599651b36793ba872870fee4f845bd484a5cac1" - integrity sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA== + version "4.7.0" + resolved "https://registry.npmjs.org/tsyringe/-/tsyringe-4.7.0.tgz" + integrity sha512-ncFDM1jTLsok4ejMvSW5jN1VGPQD48y2tfAR0pdptWRKYX4bkbqPt92k7KJ5RFJ1KV36JEs/+TMh7I6OUgj74g== dependencies: tslib "^1.9.3" type-is@~1.6.18: version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" mime-types "~2.1.24" -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== - unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== utils-merge@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== vary@^1, vary@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== diff --git a/lambda/db/src/migrations/2024.12.03T10.55.55.request-queue-attempts-column.ts b/lambda/db/src/migrations/2024.12.03T10.55.55.request-queue-attempts-column.ts new file mode 100644 index 000000000..eaa6373d1 --- /dev/null +++ b/lambda/db/src/migrations/2024.12.03T10.55.55.request-queue-attempts-column.ts @@ -0,0 +1,17 @@ +import { DataTypes } from 'sequelize'; + +export const name = '2024.12.03T10.55.55.request-queue-attempts-column'; + +export const up = async ({ context: sequelize }) => { + await sequelize.getQueryInterface().addColumn('request_queues', 'attempts', { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + }); +}; + +export const down = async ({ context: sequelize }) => { + await sequelize.getQueryInterface().removeColumn('request_queues', 'attempts'); +}; + +export default { name, up, down }; diff --git a/lambda/db/src/umzug.ts b/lambda/db/src/umzug.ts index 44781038c..74db2b5c4 100644 --- a/lambda/db/src/umzug.ts +++ b/lambda/db/src/umzug.ts @@ -58,6 +58,7 @@ export const createMigrator = async (logger?: any) => { await import('./migrations/2024.06.21T11.35.24.add-bcsc-prod-approved'), await import('./migrations/2024.07.24T00.00.00.update-survey-trigger-event'), await import('./migrations/2024.09.16T00.00.00.remove-vc-approved'), + await import('./migrations/2024.12.03T10.55.55.request-queue-attempts-column'), ], context: sequelize, storage: new SequelizeStorage({ diff --git a/lambda/db/yarn.lock b/lambda/db/yarn.lock index 41f0e40f3..90912c017 100644 --- a/lambda/db/yarn.lock +++ b/lambda/db/yarn.lock @@ -3,152 +3,152 @@ "@rushstack/ts-command-line@^4.7.7": - "integrity" "sha512-Y3GkUag39sTIlukDg9mUp8MCHrrlJ27POrBNRQGc/uF+VVgX8M7zMzHch5zP6O1QVquWgD7Engdpn2piPYaS/g==" - "resolved" "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.6.tgz" - "version" "4.10.6" + version "4.10.6" + resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.6.tgz" + integrity sha512-Y3GkUag39sTIlukDg9mUp8MCHrrlJ27POrBNRQGc/uF+VVgX8M7zMzHch5zP6O1QVquWgD7Engdpn2piPYaS/g== dependencies: "@types/argparse" "1.0.38" - "argparse" "~1.0.9" - "colors" "~1.2.1" - "string-argv" "~0.3.1" + argparse "~1.0.9" + colors "~1.2.1" + string-argv "~0.3.1" "@types/argparse@1.0.38": - "integrity" "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==" - "resolved" "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz" - "version" "1.0.38" - -"argparse@~1.0.9": - "integrity" "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==" - "resolved" "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" - "version" "1.0.10" + version "1.0.38" + resolved "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz" + integrity sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA== + +argparse@~1.0.9: + version "1.0.10" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: - "sprintf-js" "~1.0.2" + sprintf-js "~1.0.2" -"balanced-match@^1.0.0": - "integrity" "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - "version" "1.0.2" +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -"brace-expansion@^1.1.7": - "integrity" "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" - "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - "version" "1.1.11" +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: - "balanced-match" "^1.0.0" - "concat-map" "0.0.1" - -"colors@~1.2.1": - "integrity" "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==" - "resolved" "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz" - "version" "1.2.5" - -"concat-map@0.0.1": - "integrity" "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - "resolved" "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - "version" "0.0.1" - -"emittery@^0.10.2": - "integrity" "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==" - "resolved" "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz" - "version" "0.10.2" - -"fs-jetpack@^4.1.0": - "integrity" "sha512-dbeOK84F6BiQzk2yqqCVwCPWTxAvVGJ3fMQc6E2wuEohS28mR6yHngbrKuVCK1KHRx/ccByDylqu4H5PCP2urQ==" - "resolved" "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-4.3.1.tgz" - "version" "4.3.1" + balanced-match "^1.0.0" + concat-map "0.0.1" + +colors@~1.2.1: + version "1.2.5" + resolved "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz" + integrity sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +emittery@^0.10.2: + version "0.10.2" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz" + integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== + +fs-jetpack@^4.1.0: + version "4.3.1" + resolved "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-4.3.1.tgz" + integrity sha512-dbeOK84F6BiQzk2yqqCVwCPWTxAvVGJ3fMQc6E2wuEohS28mR6yHngbrKuVCK1KHRx/ccByDylqu4H5PCP2urQ== dependencies: - "minimatch" "^3.0.2" - "rimraf" "^2.6.3" - -"fs.realpath@^1.0.0": - "integrity" "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - "version" "1.0.0" - -"glob@^7.1.3", "glob@^7.1.6": - "integrity" "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==" - "resolved" "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" - "version" "7.2.0" + minimatch "^3.0.2" + rimraf "^2.6.3" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +glob@^7.1.3, glob@^7.1.6: + version "7.2.0" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: - "fs.realpath" "^1.0.0" - "inflight" "^1.0.4" - "inherits" "2" - "minimatch" "^3.0.4" - "once" "^1.3.0" - "path-is-absolute" "^1.0.0" - -"inflight@^1.0.4": - "integrity" "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" - "resolved" "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - "version" "1.0.6" + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: - "once" "^1.3.0" - "wrappy" "1" - -"inherits@2": - "integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - "version" "2.0.4" - -"minimatch@^3.0.2", "minimatch@^3.0.4": - "integrity" "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==" - "resolved" "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - "version" "3.1.2" + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +minimatch@^3.0.2, minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: - "brace-expansion" "^1.1.7" + brace-expansion "^1.1.7" -"once@^1.3.0": - "integrity" "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" - "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - "version" "1.4.0" +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: - "wrappy" "1" - -"path-is-absolute@^1.0.0": - "integrity" "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - "resolved" "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - "version" "1.0.1" - -"pony-cause@^1.1.1": - "integrity" "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==" - "resolved" "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz" - "version" "1.1.1" - -"rimraf@^2.6.3": - "integrity" "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==" - "resolved" "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" - "version" "2.7.1" + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pony-cause@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz" + integrity sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g== + +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: - "glob" "^7.1.3" - -"sprintf-js@~1.0.2": - "integrity" "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - "resolved" "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" - "version" "1.0.3" - -"string-argv@~0.3.1": - "integrity" "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==" - "resolved" "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz" - "version" "0.3.1" - -"type-fest@^2.0.0": - "integrity" "sha512-Qe5GRT+n/4GoqCNGGVp5Snapg1Omq3V7irBJB3EaKsp7HWDo5Gv2d/67gfNyV+d5EXD+x/RF5l1h4yJ7qNkcGA==" - "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-2.12.0.tgz" - "version" "2.12.0" - -"umzug@^3.1.1": - "integrity" "sha512-sgMDzUK6ZKS3pjzRJpAHqSkvAQ+64Dourq6JfQv11i0nMu0/QqE3V3AUpj2pWYxFBaSvnUxKrzZQmPr6NZhvdQ==" - "resolved" "https://registry.npmjs.org/umzug/-/umzug-3.1.1.tgz" - "version" "3.1.1" + glob "^7.1.3" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +string-argv@~0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + +type-fest@^2.0.0: + version "2.12.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.12.0.tgz" + integrity sha512-Qe5GRT+n/4GoqCNGGVp5Snapg1Omq3V7irBJB3EaKsp7HWDo5Gv2d/67gfNyV+d5EXD+x/RF5l1h4yJ7qNkcGA== + +umzug@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/umzug/-/umzug-3.1.1.tgz" + integrity sha512-sgMDzUK6ZKS3pjzRJpAHqSkvAQ+64Dourq6JfQv11i0nMu0/QqE3V3AUpj2pWYxFBaSvnUxKrzZQmPr6NZhvdQ== dependencies: "@rushstack/ts-command-line" "^4.7.7" - "emittery" "^0.10.2" - "fs-jetpack" "^4.1.0" - "glob" "^7.1.6" - "pony-cause" "^1.1.1" - "type-fest" "^2.0.0" - -"wrappy@1": - "integrity" "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - "version" "1.0.2" + emittery "^0.10.2" + fs-jetpack "^4.1.0" + glob "^7.1.6" + pony-cause "^1.1.1" + type-fest "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/lambda/jest.config.js b/lambda/jest.config.js index df31328b3..409d1b8ae 100644 --- a/lambda/jest.config.js +++ b/lambda/jest.config.js @@ -1,12 +1,20 @@ module.exports = { roots: [''], + preset: 'ts-jest/presets/js-with-ts', testMatch: ['**/?(*.)+(spec|test).+(ts|tsx|js)'], transform: { - '^.+\\.(ts|tsx)$': 'ts-jest', + // IMPORTANT: js is here intentionally to transform js files with ES Module syntax. The overriding config file allows js. + '^.+\\.(ts|tsx|js)$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.jest.json', + }, + ], }, setupFilesAfterEnv: ['./__tests__/jest.setup.js'], testSequencer: './testSequencer.js', testPathIgnorePatterns: ['/node_modules/', '/build/'], + transformIgnorePatterns: ['/node_modules/(?!(@keycloak|url-join|url-template|camelize-ts)/)'], moduleNameMapper: { '^@app/(.*)$': '/../app/$1', '^@lambda-app/(.*)$': '/app/src/$1', diff --git a/lambda/request-queue/package-lock.json b/lambda/request-queue/package-lock.json index 0a5ce66e6..f07787428 100644 --- a/lambda/request-queue/package-lock.json +++ b/lambda/request-queue/package-lock.json @@ -83,6 +83,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -242,6 +253,29 @@ "node": ">=6" } }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -316,6 +350,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "peer": true, + "requires": { + "undici-types": "~6.19.2" + } + }, "acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -408,6 +452,20 @@ "strip-bom": "^3.0.0" } }, + "typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "peer": true + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "peer": true + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/lambda/request-queue/package.json b/lambda/request-queue/package.json index 6bd1f842e..5d157b0e7 100644 --- a/lambda/request-queue/package.json +++ b/lambda/request-queue/package.json @@ -7,7 +7,7 @@ "compile": "../node_modules/.bin/tsc && ../node_modules/.bin/tsc-alias", "build": "../node_modules/.bin/ncc build src/main.ts -o dist -m", "copy-templates": "../node_modules/.bin/copyfiles -u 1 ../shared/templates/**/*.html ../shared/templates/*.html ./src/**/*.json dist --flat", - "start": "NODE_ENVIRONMENT=local ts-node src/main.ts" + "start": "NODE_ENVIRONMENT=local bun src/main.ts" }, "author": "", "license": "ISC", diff --git a/lambda/request-queue/src/main.ts b/lambda/request-queue/src/main.ts index be7d29d54..93c585a05 100644 --- a/lambda/request-queue/src/main.ts +++ b/lambda/request-queue/src/main.ts @@ -1,83 +1,72 @@ if (process.env.NODE_ENVIRONMENT === 'local') { require('dotenv').config(); } -import { models } from '../../shared/sequelize/models/models'; +import { models } from '@lambda-shared/sequelize/models/models'; import { keycloakClient } from '@lambda-app/keycloak/integration'; import { updatePlannedIntegration, createEvent } from '@lambda-app/controllers/requests'; import { ACTION_TYPES, EVENTS } from '@lambda-shared/enums'; +import axios from 'axios'; const REQUEST_QUEUE_INTERVAL_SECONDS = 60; +export const MAX_ATTEMPTS = 5; + +export const sendRcNotification = async (message) => { + try { + const headers = { Accept: 'application/json' }; + await axios.post(process.env.RC_WEBHOOK, { projectName: 'request_queue', message }, { headers }); + } catch (err) { + console.error('Unable to send RC notification', err); + } +}; export const handler = async () => { try { - const allPromises: Promise[] = []; const requestQueue = await models.requestQueue.findAll(); if (requestQueue.length === 0) { console.info('Request queue empty, exiting.'); } - requestQueue.forEach((queuedRequest) => { - const requestQueueSecondsAgo = (new Date().getTime() - new Date(queuedRequest.createdAt).getTime()) / 1000; + for (const queuedRequest of requestQueue) { + if (queuedRequest.attempts >= MAX_ATTEMPTS) { + console.info(`request ${queuedRequest.request.clientId} at maximum attempts. Skipping.`); + continue; + } + // Only act on queued items more than a minute old to prevent potential duplication. - if (requestQueueSecondsAgo < REQUEST_QUEUE_INTERVAL_SECONDS) return; + const requestQueueSecondsAgo = (new Date().getTime() - new Date(queuedRequest.createdAt).getTime()) / 1000; + if (requestQueueSecondsAgo < REQUEST_QUEUE_INTERVAL_SECONDS) continue; + console.info(`processing queued request ${queuedRequest.request.id}`); const { existingClientId, ...request } = queuedRequest.request; - // Create/update/delete each environment, based on request data. e.g if archived is true will delete. + // Handle client update for each env const environmentPromises = queuedRequest.request.environments.map((env) => keycloakClient(env, request, existingClientId), ); + const envResults = await Promise.all(environmentPromises); + + const allEnvironmentsSucceeded = envResults.every((result) => result); + const sendEmail = queuedRequest.action !== ACTION_TYPES.DELETE; // Update DB, create event and send email based on keycloak results. - allPromises.push( - Promise.all(environmentPromises).then((results) => { - const allEnvironmentsSucceeded = results.every((result) => result); - const sendEmail = queuedRequest.action !== ACTION_TYPES.DELETE; - if (allEnvironmentsSucceeded) { - const promises = Promise.all([ - models.requestQueue.destroy({ - where: { - id: queuedRequest.id, - }, - }), - models.request.update( - { - status: 'applied', - }, - { - where: { - id: queuedRequest.requestId, - }, - }, - ), - createEvent({ eventCode: EVENTS.REQUEST_APPLY_SUCCESS, requestId: request.id }), - ]) - // Must send email after event creation, since event is used to determine update vs create - .then(() => { - if (sendEmail) { - return updatePlannedIntegration(request); - } - }); - return promises as Promise; - } else { - return Promise.all([ - models.request.update( - { - status: 'applyFailed', - }, - { - where: { - id: queuedRequest.requestId, - }, - }, - ), - createEvent({ eventCode: EVENTS.REQUEST_APPLY_FAILURE, requestId: request.id }), - ]); - } - }), - ); - }); - await Promise.all(allPromises); + if (allEnvironmentsSucceeded) { + await models.request.update({ status: 'applied' }, { where: { id: queuedRequest.requestId } }); + await models.requestQueue.destroy({ where: { id: queuedRequest.id } }); + await createEvent({ eventCode: EVENTS.REQUEST_APPLY_SUCCESS, requestId: request.id }); + if (sendEmail) await updatePlannedIntegration(request); + } else { + await models.requestQueue.update({ attempts: queuedRequest.attempts + 1 }, { where: { id: queuedRequest.id } }); + await models.request.update({ status: 'applyFailed' }, { where: { id: queuedRequest.requestId } }); + await createEvent({ eventCode: EVENTS.REQUEST_APPLY_FAILURE, requestId: request.id }); + } + if (queuedRequest.attempts >= MAX_ATTEMPTS - 1) { + let message = `Request ${queuedRequest.request.clientId} has reached maximum retries and requires manual intervention.`; + if (process.env.NODE_ENV === 'development') { + message = 'SANDBOX: ' + message; + } + await sendRcNotification(message); + } + } } catch (err) { console.error(err); } diff --git a/lambda/request-queue/yarn.lock b/lambda/request-queue/yarn.lock index dd671817d..cc8190f9f 100644 --- a/lambda/request-queue/yarn.lock +++ b/lambda/request-queue/yarn.lock @@ -4,104 +4,109 @@ "@cspotcode/source-map-support@^0.8.0": version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== dependencies: "@jridgewell/trace-mapping" "0.3.9" "@jridgewell/resolve-uri@^3.0.3": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + version "3.1.1" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== "@jridgewell/sourcemap-codec@^1.4.10": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + version "1.4.15" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== "@jridgewell/trace-mapping@0.3.9": version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" "@tsconfig/node10@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" - integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + version "1.0.9" + resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== "@tsconfig/node12@^1.0.7": version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== "@tsconfig/node14@^1.0.0": version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== "@tsconfig/node16@^1.0.2": version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -acorn-walk@^8.1.1: - version "8.3.4" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" - integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== +"@types/node@*": + version "22.7.9" + resolved "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz" + integrity sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg== dependencies: - acorn "^8.11.0" + undici-types "~6.19.2" -acorn@^8.11.0, acorn@^8.4.1: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +acorn-walk@^8.1.1: + version "8.3.1" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz" + integrity sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw== + +acorn@^8.4.1: + version "8.11.3" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== arg@^4.1.0: version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== create-require@^1.1.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== diff@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== dotenv@^16.3.1: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + version "16.3.1" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== json5@^2.2.2: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== make-error@^1.1.1: version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== minimist@^1.2.6: version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== ts-node@^10.9.2: version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== dependencies: "@cspotcode/source-map-support" "^0.8.0" @@ -120,19 +125,29 @@ ts-node@^10.9.2: tsconfig-paths@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== dependencies: json5 "^2.2.2" minimist "^1.2.6" strip-bom "^3.0.0" +typescript@>=2.7: + version "5.6.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + v8-compile-cache-lib@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== yn@3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/lambda/shared/sequelize/config/config.ts b/lambda/shared/sequelize/config/config.ts index 35bcd0e7c..15f7d704e 100644 --- a/lambda/shared/sequelize/config/config.ts +++ b/lambda/shared/sequelize/config/config.ts @@ -10,6 +10,11 @@ const config = { dialect: 'postgres', dialectModule: pg, use_env_variable: 'DATABASE_URL', + host: process.env.DB_HOSTNAME, + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + logging: false, }, test: { dialect: 'postgres', diff --git a/lambda/shared/sequelize/models/RequestQueue.ts b/lambda/shared/sequelize/models/RequestQueue.ts index dd3030437..c67defd52 100644 --- a/lambda/shared/sequelize/models/RequestQueue.ts +++ b/lambda/shared/sequelize/models/RequestQueue.ts @@ -18,6 +18,11 @@ const init = (sequelize, DataTypes) => { type: DataTypes.JSONB, allowNull: true, }, + attempts: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + }, }, { underscored: true, diff --git a/lambda/shared/templates/delete-inactive-idir-users/delete-inactive-idir-users.html b/lambda/shared/templates/delete-inactive-idir-users/delete-inactive-idir-users.html index 606afa86d..a63ce1efa 100644 --- a/lambda/shared/templates/delete-inactive-idir-users/delete-inactive-idir-users.html +++ b/lambda/shared/templates/delete-inactive-idir-users/delete-inactive-idir-users.html @@ -1,7 +1,7 @@

Dear Pathfinder SSO friend,

The IDIR user with the idir username {{username}} has an inactive guid in our system. They have been removed from - client {{clientId}}{{#if roles.length}} and associated roles {{roles}}{{/if}} + client {{clientId}} in the {{env}} environment{{#if roles.length}} and associated roles {{roles}}{{/if}}

{{#if teamAdmin}}

diff --git a/lambda/siteminder-tests-scheduler/yarn.lock b/lambda/siteminder-tests-scheduler/yarn.lock index 1ab9d6c0c..9c01b9b5e 100644 --- a/lambda/siteminder-tests-scheduler/yarn.lock +++ b/lambda/siteminder-tests-scheduler/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@octokit/types" "^6.0.3" -"@octokit/core@^3.5.1": +"@octokit/core@^3.5.1", "@octokit/core@>=2", "@octokit/core@>=3": version "3.5.1" resolved "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz" integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== diff --git a/lambda/tsconfig.jest.json b/lambda/tsconfig.jest.json new file mode 100644 index 000000000..1809033ff --- /dev/null +++ b/lambda/tsconfig.jest.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": true + } +} diff --git a/lambda/yarn.lock b/lambda/yarn.lock index 1570c8907..11d58b059 100644 --- a/lambda/yarn.lock +++ b/lambda/yarn.lock @@ -23,7 +23,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz" integrity sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ== -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8": version "7.25.2" resolved "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz" integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== @@ -498,7 +498,7 @@ jest-haste-map "^29.7.0" slash "^3.0.0" -"@jest/transform@^29.7.0": +"@jest/transform@^29.0.0", "@jest/transform@^29.7.0": version "29.7.0" resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== @@ -575,14 +575,6 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.9": version "0.3.25" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" @@ -591,6 +583,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@kurkle/color@^0.3.0": version "0.3.2" resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz" @@ -604,7 +604,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -617,6 +617,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@popperjs/core@^2.11.6": + version "2.11.8" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@sinclair/typebox@^0.24.1": version "0.24.38" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.38.tgz" @@ -1038,7 +1043,7 @@ ajv-errors@^3.0.0: resolved "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz" integrity sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ== -ajv@^8.17.1: +ajv@^8.0.1, ajv@^8.17.1: version "8.17.1" resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== @@ -1169,7 +1174,7 @@ axios@^1.7.7: form-data "^4.0.0" proxy-from-env "^1.1.0" -babel-jest@^29.7.0: +babel-jest@^29.0.0, babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== @@ -1244,10 +1249,10 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== +body-parser@^1.20.0: + version "1.20.0" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== dependencies: bytes "3.1.2" content-type "~1.0.4" @@ -1257,15 +1262,15 @@ body-parser@1.20.1: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.10.3" raw-body "2.5.1" type-is "~1.6.18" unpipe "1.0.0" -body-parser@^1.20.0: - version "1.20.0" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz" - integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== dependencies: bytes "3.1.2" content-type "~1.0.4" @@ -1275,7 +1280,7 @@ body-parser@^1.20.0: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.10.3" + qs "6.11.0" raw-body "2.5.1" type-is "~1.6.18" unpipe "1.0.0" @@ -1307,7 +1312,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.23.1: +browserslist@^4.23.1, "browserslist@>= 4.21.0": version "4.23.3" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz" integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== @@ -1479,16 +1484,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1608,10 +1613,12 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" debug@2.6.9: version "2.6.9" @@ -1620,13 +1627,6 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - dedent@^1.0.0: version "1.5.3" resolved "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz" @@ -1683,6 +1683,16 @@ diff-sequences@^29.6.3: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +diff@5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + diff2html@^3.1.18: version "3.4.35" resolved "https://registry.npmjs.org/diff2html/-/diff2html-3.4.35.tgz" @@ -1693,16 +1703,6 @@ diff2html@^3.1.18: optionalDependencies: highlight.js "11.6.0" -diff@5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz" - integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -1922,7 +1922,7 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -1951,14 +1951,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - filelist@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz" @@ -2022,13 +2014,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - formidable@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz" @@ -2250,7 +2235,7 @@ html-escaper@^2.0.0: resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -http-errors@2.0.0, http-errors@^2.0.0: +http-errors@^2.0.0, http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -2278,16 +2263,16 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@1.1.13: - version "1.1.13" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - ieee754@^1.1.4: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + ignore@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" @@ -2319,7 +2304,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3, inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2490,16 +2475,16 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -2836,7 +2821,7 @@ jest-resolve-dependencies@^29.7.0: jest-regex-util "^29.6.3" jest-snapshot "^29.7.0" -jest-resolve@^29.7.0: +jest-resolve@*, jest-resolve@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== @@ -3023,7 +3008,7 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.7.0: +jest@^29.0.0, jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== @@ -3071,9 +3056,9 @@ json-schema-traverse@^1.0.0: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json5@>=2.2.2, json5@^2.2.3: +json5@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== kleur@^3.0.3: @@ -3348,21 +3333,21 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimist@>=1.2.6, minimist@^1.2.5: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" - integrity sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew== +minimist@^1.2.5: + version "1.2.6" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + integrity sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew== + moment-timezone@^0.5.35: version "0.5.41" resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.41.tgz" @@ -3370,10 +3355,10 @@ moment-timezone@^0.5.35: dependencies: moment "^2.29.4" -moment@>=2.29.2, moment@^2.27.0, moment@^2.29.1, moment@^2.29.4: - version "2.30.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" - integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== +moment@^2.27.0, moment@^2.29.1, moment@^2.29.4: + version "2.29.4" + resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== ms@2.0.0: version "2.0.0" @@ -3415,20 +3400,6 @@ neo-async@^2.6.0: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - -node-fetch@>=2.6.7: - version "3.3.2" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" - integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -3512,7 +3483,14 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -p-limit@^2.0.0, p-limit@^2.2.0: +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -3636,7 +3614,7 @@ pg-types@^2.1.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@^8.7.3: +pg@^8.7.3, pg@>=8.0: version "8.8.0" resolved "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz" integrity sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw== @@ -3774,6 +3752,13 @@ pure-rand@^6.0.0: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +qs@^6.11.0, qs@6.11.0: + version "6.11.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + qs@6.10.3: version "6.10.3" resolved "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz" @@ -3781,13 +3766,6 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" -qs@6.11.0, qs@^6.11.0: - version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - querystring@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" @@ -3920,7 +3898,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.2.1, safe-buffer@^5.1.2: +safe-buffer@^5.1.2, safe-buffer@5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -3935,16 +3913,16 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" - integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== - sax@>=0.6.0: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +sax@1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" + integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== + semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" @@ -3964,7 +3942,17 @@ semver@^7.3.8: dependencies: lru-cache "^6.0.0" -semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: +semver@^7.5.3: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +semver@^7.5.4: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +semver@^7.6.3: version "7.6.3" resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -4101,6 +4089,18 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -4136,18 +4136,6 @@ string.prototype.trimstart@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -4279,7 +4267,7 @@ ts-jest@^29.2.5: semver "^7.6.3" yargs-parser "^21.1.1" -ts-node@^10.8.1: +ts-node@^10.8.1, ts-node@>=9.0.0: version "10.9.1" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz" integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== @@ -4328,7 +4316,7 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typescript@^4.8.2: +typescript@^4.8.2, typescript@>=2.7, "typescript@>=4.3 <6": version "4.8.2" resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz" integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== @@ -4348,7 +4336,7 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -4396,16 +4384,16 @@ utils-merge@1.0.1: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== - uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" @@ -4445,11 +4433,6 @@ watchpack@^2.0.0-beta.10: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -web-streams-polyfill@^3.0.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" - integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" diff --git a/localserver/bunfig.toml b/localserver/bunfig.toml new file mode 100644 index 000000000..8779d8ccc --- /dev/null +++ b/localserver/bunfig.toml @@ -0,0 +1 @@ +preload = ["./reflect-metadata-import.ts"] diff --git a/localserver/express-server.ts b/localserver/express-server.ts index 16365e0f7..fd105bc64 100644 --- a/localserver/express-server.ts +++ b/localserver/express-server.ts @@ -21,7 +21,7 @@ const initExpresss = async () => { expressServer.use(bodyParser.json()); expressServer.use(bodyParser.urlencoded({ extended: false })); expressServer.use(cookieParser()); - expressServer.use(cors({ origin: 'http://localhost:3000', credentials: true })); + expressServer.use(cors({ origin: 'http://localhost:3000', credentials: true, exposedHeaders: ['X-Message'] })); expressServer.disable('x-powered-by'); expressServer.set('trust proxy', 1); diff --git a/localserver/package.json b/localserver/package.json index bc561a621..e3c35fa42 100644 --- a/localserver/package.json +++ b/localserver/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "author": "SSO Team", "scripts": { - "dev": "nodemon --trace-warnings", + "dev": "bun --hot server.ts", "migrate-db": "nodemon --config nodemon-db.json" }, "devDependencies": { @@ -26,14 +26,6 @@ "resolutions": { "minimist": ">=1.2.6" }, - "nodemonConfig": { - "watch": [ - "./", - "../lambda" - ], - "ext": "ts,json", - "exec": "ts-node -r tsconfig-paths/register -r dotenv/config ./server.ts" - }, "dependencies": { "axios": "^1.7.7", "cors": "^2.8.5" diff --git a/localserver/reflect-metadata-import.ts b/localserver/reflect-metadata-import.ts new file mode 100644 index 000000000..d2c9bc6e6 --- /dev/null +++ b/localserver/reflect-metadata-import.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; diff --git a/localserver/yarn.lock b/localserver/yarn.lock index b0257e932..814211821 100644 --- a/localserver/yarn.lock +++ b/localserver/yarn.lock @@ -195,13 +195,13 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== +body-parser@^1.20.0: + version "1.20.2" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" @@ -209,17 +209,17 @@ body-parser@1.20.1: iconv-lite "0.4.24" on-finished "2.4.1" qs "6.11.0" - raw-body "2.5.1" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" -body-parser@^1.20.0: - version "1.20.2" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== dependencies: bytes "3.1.2" - content-type "~1.0.5" + content-type "~1.0.4" debug "2.6.9" depd "2.0.0" destroy "1.2.0" @@ -227,7 +227,7 @@ body-parser@^1.20.0: iconv-lite "0.4.24" on-finished "2.4.1" qs "6.11.0" - raw-body "2.5.2" + raw-body "2.5.1" type-is "~1.6.18" unpipe "1.0.0" @@ -339,13 +339,6 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -353,12 +346,19 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -depd@2.0.0, depd@~2.0.0: +depd@~2.0.0, depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -634,9 +634,9 @@ minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@>=1.2.6, minimist@^1.2.6: +minimist@^1.2.6: version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== morgan@^1.10.0: @@ -650,16 +650,16 @@ morgan@^1.10.0: on-finished "~2.3.0" on-headers "~1.0.2" +ms@^2.1.1, ms@2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.3, ms@^2.1.1: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - negotiator@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" @@ -703,13 +703,6 @@ object-inspect@^1.9.0: resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" @@ -717,6 +710,13 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + on-headers@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" @@ -946,7 +946,7 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typescript@^4.8.2: +typescript@^4.8.2, typescript@>=2.7: version "4.9.5" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== @@ -956,7 +956,7 @@ undefsafe@^2.0.5: resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== diff --git a/package.json b/package.json index 96af4dfd9..985c04cb5 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,6 @@ "author": "SSO Team", "devDependencies": { "axios": "^0.27.2" - } + }, + "dependencies": {} } diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl index b698b6bdb..2ead96017 100644 --- a/terraform/.terraform.lock.hcl +++ b/terraform/.terraform.lock.hcl @@ -2,44 +2,44 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "5.31.0" + version = "5.72.1" constraints = ">= 3.30.0, >= 4.34.0" hashes = [ - "h1:ltxyuBWIy9cq0kIKDJH1jeWJy/y7XJLjS4QrsQK4plA=", - "zh:0cdb9c2083bf0902442384f7309367791e4640581652dda456f2d6d7abf0de8d", - "zh:2fe4884cb9642f48a5889f8dff8f5f511418a18537a9dfa77ada3bcdad391e4e", - "zh:36d8bdd72fe61d816d0049c179f495bc6f1e54d8d7b07c45b62e5e1696882a89", - "zh:539dd156e3ec608818eb21191697b230117437a58587cbd02ce533202a4dd520", - "zh:6a53f4b57ac4eb3479fc0d8b6e301ca3a27efae4c55d9f8bd24071b12a03361c", - "zh:6faeb8ff6792ca7af1c025255755ad764667a300291cc10cea0c615479488c87", - "zh:7d9423149b323f6d0df5b90c4d9029e5455c670aea2a7eb6fef4684ba7eb2e0b", - "zh:8235badd8a5d0993421cacf5ead48fac73d3b5a25c8a68599706a404b1f70730", - "zh:860b4f60842b2879c5128b7e386c8b49adeda9287fed12c5cd74861bb659bbcd", + "h1:jhd5O5o0CfZCNEwwN0EiDAzb7ApuFrtxJqa6HXW4EKE=", + "zh:0dea6843836e926d33469b48b948744079023816d16a2ff7666bcfb6aa3522d4", + "zh:195fa9513f75800a0d62797ebec75ee73e9b8c28d713fe9b63d3b1d1eec129b3", + "zh:1ed92f3961715bf0e024bcde3c12dfbdc50b00c1f8a43cc00802cfc45a256208", + "zh:2ac687e3a52606466cae4a6813e81d923042488df88d2424e28d3f8530f091bb", + "zh:32e7ca75f9314557daada3c44628fe1f3bf964a4f833bfb4b2295d833fe64b6f", + "zh:374ee0e6b4327cc6ef666908ce5d6450a3a56e90cd2b785e83c2bcfc100021d2", + "zh:5500fd6fdac44f96411fcf9c6d01691159ec35455ed127eb4c3a498e1cc92a64", + "zh:723a2dc4b064c12e7ee62ad4fbfd72fa5e025206ea47b735994ef53f3c373152", + "zh:89d97b87605f1d734f27e642567cbecf785b521af8ea81dac55c77ccde876221", + "zh:951ee1e5731e8d65d521d71b95927e55055b3c4656eef6d46fa580a63328befc", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:b021fceaf9382c8fe3c6eb608c24d01dce3d11ba7e65bb443d51ca9b90e9b237", - "zh:b38b0bfc1c69e714e80cf1c9ea06e687ee86aa9f45694be28eb07adcebbe0489", - "zh:c972d155f6c01af9690a72adfb99cfc24ef5ef311ca92ce46b9b13c5c153f572", - "zh:e0dd29920ec84fdb6026acff44dcc1fb1a24a0caa093fa04cdbc713d384c651d", - "zh:e3127ebd2cb0374cd1808f911e6bffe2f4ac4d84317061381242353f3a7bc27d", + "zh:9b2b362470b64ec227b2da64762ab8bc4111c6b80365fd9d82fc5e1e33f44038", + "zh:aa6e57d0cb974ff0da5dee5d43ad2745cbbc4a2b507d4c799839b9fa96daf688", + "zh:ba0d14c4a6b7aa844a830d47c0bf995b632e37f0795394b5b60c638b62b7fc03", + "zh:c9764065a9c5d324db0b02bd201b9e3a2118e49c4960884acdeea377173302e9", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.6.0" + version = "3.6.3" constraints = ">= 2.2.0" hashes = [ - "h1:I8MBeauYA8J8yheLJ8oSMWqB0kovn16dF/wKZ1QTdkk=", - "zh:03360ed3ecd31e8c5dac9c95fe0858be50f3e9a0d0c654b5e504109c2159287d", - "zh:1c67ac51254ba2a2bb53a25e8ae7e4d076103483f55f39b426ec55e47d1fe211", - "zh:24a17bba7f6d679538ff51b3a2f378cedadede97af8a1db7dad4fd8d6d50f829", - "zh:30ffb297ffd1633175d6545d37c2217e2cef9545a6e03946e514c59c0859b77d", - "zh:454ce4b3dbc73e6775f2f6605d45cee6e16c3872a2e66a2c97993d6e5cbd7055", + "h1:zG9uFP8l9u+yGZZvi5Te7PV62j50azpgwPunq2vTm1E=", + "zh:04ceb65210251339f07cd4611885d242cd4d0c7306e86dda9785396807c00451", + "zh:448f56199f3e99ff75d5c0afacae867ee795e4dfda6cb5f8e3b2a72ec3583dd8", + "zh:4b4c11ccfba7319e901df2dac836b1ae8f12185e37249e8d870ee10bb87a13fe", + "zh:4fa45c44c0de582c2edb8a2e054f55124520c16a39b2dfc0355929063b6395b1", + "zh:588508280501a06259e023b0695f6a18149a3816d259655c424d068982cbdd36", + "zh:737c4d99a87d2a4d1ac0a54a73d2cb62974ccb2edbd234f333abd079a32ebc9e", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:91df0a9fab329aff2ff4cf26797592eb7a3a90b4a0c04d64ce186654e0cc6e17", - "zh:aa57384b85622a9f7bfb5d4512ca88e61f22a9cea9f30febaa4c98c68ff0dc21", - "zh:c4a3e329ba786ffb6f2b694e1fd41d413a7010f3a53c20b432325a94fa71e839", - "zh:e2699bc9116447f96c53d55f2a00570f982e6f9935038c3810603572693712d0", - "zh:e747c0fd5d7684e5bfad8aa0ca441903f15ae7a98a737ff6aca24ba223207e2c", - "zh:f1ca75f417ce490368f047b63ec09fd003711ae48487fba90b4aba2ccf71920e", + "zh:a357ab512e5ebc6d1fda1382503109766e21bbfdfaa9ccda43d313c122069b30", + "zh:c51bfb15e7d52cc1a2eaec2a903ac2aff15d162c172b1b4c17675190e8147615", + "zh:e0951ee6fa9df90433728b96381fb867e3db98f66f735e0c3e24f8f16903f0ad", + "zh:e3cdcb4e73740621dabd82ee6a37d6cfce7fee2a03d8074df65086760f5cf556", + "zh:eff58323099f1bd9a0bec7cb04f717e7f1b2774c7d612bf7581797e1622613a0", ] } diff --git a/terraform/alb.tf b/terraform/alb.tf index 8a3c0327b..4e484997d 100644 --- a/terraform/alb.tf +++ b/terraform/alb.tf @@ -5,29 +5,28 @@ resource "aws_alb" "sso_alb" { security_groups = [data.aws_security_group.web.id] subnets = [data.aws_subnet.a.id, data.aws_subnet.b.id] enable_cross_zone_load_balancing = true - tags = var.sso_grafana_tags lifecycle { ignore_changes = [access_logs] } } -resource "aws_alb_listener" "alb_listener_sso_grafana" { - count = var.install_sso_css_grafana +resource "aws_alb_listener" "grafana" { + count = var.install_grafana load_balancer_arn = aws_alb.sso_alb.arn port = "80" protocol = "HTTP" default_action { type = "forward" - target_group_arn = aws_alb_target_group.alb_target_group_sso_grafana[count.index].arn + target_group_arn = aws_alb_target_group.grafana[count.index].arn } } -resource "aws_alb_target_group" "alb_target_group_sso_grafana" { - count = var.install_sso_css_grafana - name = "${var.sso_grafana_name}-tg" - port = var.sso_grafana_container_port +resource "aws_alb_target_group" "grafana" { + count = var.install_grafana + name = "grafana" + port = 3000 protocol = "HTTP" vpc_id = data.aws_vpc.selected.id target_type = "ip" @@ -39,8 +38,8 @@ resource "aws_alb_target_group" "alb_target_group_sso_grafana" { protocol = "HTTP" matcher = "200" timeout = "3" - path = var.sso_grafana_health_check_path + path = "/api/health" unhealthy_threshold = "2" } - tags = var.sso_grafana_tags + tags = var.grafana_tags } diff --git a/terraform/api-gateway-grafana.tf b/terraform/api-gateway-grafana.tf index 6db364f43..6231a8237 100644 --- a/terraform/api-gateway-grafana.tf +++ b/terraform/api-gateway-grafana.tf @@ -1,36 +1,36 @@ -resource "aws_apigatewayv2_api" "sso_grafana_api" { - count = var.install_sso_css_grafana - name = var.sso_grafana_name +resource "aws_apigatewayv2_api" "grafana" { + count = var.install_grafana + name = "grafana" protocol_type = "HTTP" } -resource "aws_apigatewayv2_vpc_link" "sso_grafana_vpc_link" { - count = var.install_sso_css_grafana - name = var.sso_grafana_name +resource "aws_apigatewayv2_vpc_link" "grafana" { + count = var.install_grafana + name = "grafana" subnet_ids = [data.aws_subnet.a.id, data.aws_subnet.b.id] security_group_ids = [data.aws_security_group.app.id] } -resource "aws_apigatewayv2_integration" "sso_grafana_api_integration" { - count = var.install_sso_css_grafana - api_id = aws_apigatewayv2_api.sso_grafana_api[count.index].id +resource "aws_apigatewayv2_integration" "grafana" { + count = var.install_grafana + api_id = aws_apigatewayv2_api.grafana[count.index].id integration_type = "HTTP_PROXY" - connection_id = aws_apigatewayv2_vpc_link.sso_grafana_vpc_link[count.index].id + connection_id = aws_apigatewayv2_vpc_link.grafana[count.index].id connection_type = "VPC_LINK" integration_method = "ANY" - integration_uri = aws_alb_listener.alb_listener_sso_grafana[count.index].arn + integration_uri = aws_alb_listener.grafana[count.index].arn } -resource "aws_apigatewayv2_route" "sso_grafana_route_any" { - count = var.install_sso_css_grafana - api_id = aws_apigatewayv2_api.sso_grafana_api[count.index].id +resource "aws_apigatewayv2_route" "grafana" { + count = var.install_grafana + api_id = aws_apigatewayv2_api.grafana[count.index].id route_key = "ANY /{proxy+}" - target = "integrations/${aws_apigatewayv2_integration.sso_grafana_api_integration[count.index].id}" + target = "integrations/${aws_apigatewayv2_integration.grafana[count.index].id}" } -resource "aws_apigatewayv2_stage" "sso_grafana_api_default_stage" { - count = var.install_sso_css_grafana - api_id = aws_apigatewayv2_api.sso_grafana_api[count.index].id +resource "aws_apigatewayv2_stage" "grafana" { + count = var.install_grafana + api_id = aws_apigatewayv2_api.grafana[count.index].id name = "$default" auto_deploy = true } diff --git a/terraform/api-gateway.tf b/terraform/api-gateway.tf index 721c6051b..8a884c485 100644 --- a/terraform/api-gateway.tf +++ b/terraform/api-gateway.tf @@ -5,6 +5,8 @@ resource "aws_api_gateway_rest_api" "sso_backend" { name = "SSOApi" description = "Terraform Serverless Application Example" + # Compress for 25Kb and larger responses + minimum_compression_size = 25000 endpoint_configuration { types = ["REGIONAL"] diff --git a/terraform/ecs.tf b/terraform/ecs.tf index 97bf3b389..70e4e9e03 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -1,12 +1,18 @@ +locals { + grafana_cpu = (var.app_env == "production" ? 256 : 256) + grafana_memory = (var.app_env == "production" ? 512 : 512) + redis_cpu = (var.app_env == "production" ? 256 : 256) + redis_memory = (var.app_env == "production" ? 512 : 512) + grafana_port = 3000 + redis_port = 6379 +} + resource "aws_ecs_cluster" "sso_ecs_cluster" { - count = var.install_sso_css_grafana - name = "sso-ecs-cluster" - tags = var.sso_grafana_tags + name = "sso-ecs-cluster" } resource "aws_ecs_cluster_capacity_providers" "sso_ecs_cluster_capacity_providers" { - count = var.install_sso_css_grafana - cluster_name = aws_ecs_cluster.sso_ecs_cluster[count.index].name + cluster_name = aws_ecs_cluster.sso_ecs_cluster.name capacity_providers = ["FARGATE_SPOT"] default_capacity_provider_strategy { weight = 100 @@ -14,17 +20,17 @@ resource "aws_ecs_cluster_capacity_providers" "sso_ecs_cluster_capacity_provider } } -resource "aws_ecs_task_definition" "sso_grafana_task_definition" { - count = var.install_sso_css_grafana - depends_on = [aws_apigatewayv2_api.sso_grafana_api] - family = var.sso_grafana_name - execution_role_arn = aws_iam_role.ecs_sso_grafana_task_execution_role[0].arn - task_role_arn = aws_iam_role.sso_grafana_container_role[count.index].arn +resource "aws_ecs_task_definition" "grafana" { + count = var.install_grafana + depends_on = [aws_apigatewayv2_api.grafana] + family = "grafana" + execution_role_arn = aws_iam_role.grafana_task_execution_role[0].arn + task_role_arn = aws_iam_role.grafana_container_role[count.index].arn network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] - cpu = var.sso_grafana_fargate_cpu - memory = var.sso_grafana_fargate_memory - tags = var.sso_grafana_tags + cpu = local.grafana_cpu + memory = local.grafana_memory + tags = var.grafana_tags volume { name = "sso-grafana-data" efs_volume_configuration { @@ -38,17 +44,18 @@ resource "aws_ecs_task_definition" "sso_grafana_task_definition" { } container_definitions = jsonencode([ { - essential = true - name = var.sso_grafana_container_name - image = "${var.aws_ecr_uri}/${var.sso_grafana_container_image}" - cpu = var.sso_grafana_fargate_cpu - memory = var.sso_grafana_fargate_memory - networkMode = "awsvpc" + essential = true + name = "grafana" + image = "${var.aws_ecr_uri}/bcgov-sso/grafana:10.2.2" + cpu = local.grafana_cpu + memory = local.grafana_memory + readonlyRootFilesystem = true + networkMode = "awsvpc" portMappings = [ { protocol = "tcp" - containerPort = var.sso_grafana_container_port - hostPort = var.sso_grafana_container_port + containerPort = local.grafana_port + hostPort = local.grafana_port } ] environment = [ @@ -61,7 +68,7 @@ resource "aws_ecs_task_definition" "sso_grafana_task_definition" { logDriver = "awslogs" options = { awslogs-create-group = "true" - awslogs-group = "/ecs/${var.sso_grafana_name}" + awslogs-group = "/ecs/grafana" awslogs-region = "ca-central-1" awslogs-stream-prefix = "ecs" } @@ -80,11 +87,11 @@ resource "aws_ecs_task_definition" "sso_grafana_task_definition" { }, { name = "GF_SERVER_DOMAIN", - value = "${aws_apigatewayv2_api.sso_grafana_api[count.index].id}.execute-api.ca-central-1.amazonaws.com" + value = "${aws_apigatewayv2_api.grafana[count.index].id}.execute-api.ca-central-1.amazonaws.com" }, { name = "GF_SERVER_ROOT_URL", - value = "https://${aws_apigatewayv2_api.sso_grafana_api[count.index].id}.execute-api.ca-central-1.amazonaws.com" + value = "https://${aws_apigatewayv2_api.grafana[count.index].id}.execute-api.ca-central-1.amazonaws.com" }, { name = "GF_AUTH_GENERIC_OAUTH_NAME", @@ -146,27 +153,27 @@ resource "aws_ecs_task_definition" "sso_grafana_task_definition" { secrets = [ { name = "GF_SECURITY_ADMIN_PASSWORD", - valueFrom = "${data.aws_secretsmanager_secret_version.sso_grafana_secret[0].arn}:GF_SECURITY_ADMIN_PASSWORD::" + valueFrom = "${data.aws_secretsmanager_secret_version.grafana_secret[0].arn}:GF_SECURITY_ADMIN_PASSWORD::" }, { name = "GF_AUTH_GENERIC_OAUTH_CLIENT_ID", - valueFrom = "${data.aws_secretsmanager_secret_version.sso_grafana_secret[0].arn}:GF_AUTH_GENERIC_OAUTH_CLIENT_ID::" + valueFrom = "${data.aws_secretsmanager_secret_version.grafana_secret[0].arn}:GF_AUTH_GENERIC_OAUTH_CLIENT_ID::" }, { name = "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET", - valueFrom = "${data.aws_secretsmanager_secret_version.sso_grafana_secret[0].arn}:GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET::" + valueFrom = "${data.aws_secretsmanager_secret_version.grafana_secret[0].arn}:GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET::" }, ] } ]) } -resource "aws_ecs_service" "sso_grafana_service" { - count = var.install_sso_css_grafana - name = var.sso_grafana_name - cluster = aws_ecs_cluster.sso_ecs_cluster[count.index].id - task_definition = aws_ecs_task_definition.sso_grafana_task_definition[count.index].arn - desired_count = var.install_sso_css_grafana +resource "aws_ecs_service" "grafana" { + count = var.install_grafana + name = "grafana" + cluster = aws_ecs_cluster.sso_ecs_cluster.id + task_definition = aws_ecs_task_definition.grafana[count.index].arn + desired_count = 1 enable_ecs_managed_tags = true propagate_tags = "TASK_DEFINITION" health_check_grace_period_seconds = 60 @@ -186,17 +193,96 @@ resource "aws_ecs_service" "sso_grafana_service" { } load_balancer { - target_group_arn = aws_alb_target_group.alb_target_group_sso_grafana[count.index].id - container_name = var.sso_grafana_container_name - container_port = var.sso_grafana_container_port + target_group_arn = aws_alb_target_group.grafana[count.index].id + container_name = "grafana" + container_port = local.grafana_port } - depends_on = [aws_iam_role_policy_attachment.ecs_sso_grafana_task_role_policy_attachment] + depends_on = [aws_iam_role_policy_attachment.grafana] - tags = var.sso_grafana_tags + tags = var.grafana_tags } -data "aws_secretsmanager_secret_version" "sso_grafana_secret" { - count = var.install_sso_css_grafana +data "aws_secretsmanager_secret_version" "grafana_secret" { + count = var.install_grafana secret_id = "sso-grafana" } + +resource "aws_ecs_task_definition" "redis" { + count = var.install_redis + depends_on = [aws_lambda_function.app] + family = "redis" + execution_role_arn = aws_iam_role.redis_task_execution_role[0].arn + task_role_arn = aws_iam_role.redis_container_role[count.index].arn + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = local.redis_cpu + memory = local.redis_memory + tags = var.redis_tags + container_definitions = jsonencode([ + { + essential = true + name = "redis" + image = "public.ecr.aws/docker/library/redis:latest" + cpu = local.redis_cpu + memory = local.redis_memory + readonlyRootFilesystem = true + networkMode = "awsvpc" + portMappings = [ + { + protocol = "tcp" + containerPort = local.redis_port + hostPort = local.redis_port + } + ] + environment = [ + { + name = "AWS_REGION", + value = "ca-central-1" + } + ] + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-create-group = "true" + awslogs-group = "/ecs/redis" + awslogs-region = "ca-central-1" + awslogs-stream-prefix = "ecs" + } + } + } + ]) +} + +resource "aws_ecs_service" "redis" { + count = var.install_redis + name = "redis" + cluster = aws_ecs_cluster.sso_ecs_cluster.id + task_definition = aws_ecs_task_definition.redis[count.index].arn + desired_count = var.install_redis + enable_ecs_managed_tags = true + propagate_tags = "TASK_DEFINITION" + health_check_grace_period_seconds = 60 + wait_for_steady_state = false + + + capacity_provider_strategy { + capacity_provider = "FARGATE_SPOT" + weight = 100 + } + + + network_configuration { + security_groups = [data.aws_security_group.app.id] + subnets = [data.aws_subnet.a.id, data.aws_subnet.b.id] + assign_public_ip = false + } + + load_balancer { + target_group_arn = aws_lb_target_group.redis[count.index].id + container_name = "redis" + container_port = local.redis_port + } + + tags = var.redis_tags +} diff --git a/terraform/efs.tf b/terraform/efs.tf index a26837b0b..3200c455e 100644 --- a/terraform/efs.tf +++ b/terraform/efs.tf @@ -1,5 +1,5 @@ resource "aws_efs_file_system" "efs_sso_grafana" { - count = var.install_sso_css_grafana + count = var.install_grafana creation_token = "efs-sso-grafana" encrypted = true @@ -7,26 +7,26 @@ resource "aws_efs_file_system" "efs_sso_grafana" { { Name = "efs-sso-grafana" }, - var.sso_grafana_tags + var.grafana_tags ) } resource "aws_efs_mount_target" "efs_sso_grafana_azA" { - count = var.install_sso_css_grafana + count = var.install_grafana file_system_id = aws_efs_file_system.efs_sso_grafana[count.index].id subnet_id = data.aws_subnet.a_data.id security_groups = [data.aws_security_group.app.id] } resource "aws_efs_mount_target" "efs_sso_grafana_azB" { - count = var.install_sso_css_grafana + count = var.install_grafana file_system_id = aws_efs_file_system.efs_sso_grafana[count.index].id subnet_id = data.aws_subnet.b_data.id security_groups = [data.aws_security_group.app.id] } resource "aws_efs_backup_policy" "efs_sso_grafana_backups_policy" { - count = var.install_sso_css_grafana + count = var.install_grafana file_system_id = aws_efs_file_system.efs_sso_grafana[count.index].id backup_policy { @@ -35,7 +35,7 @@ resource "aws_efs_backup_policy" "efs_sso_grafana_backups_policy" { } resource "aws_efs_access_point" "sso_grafana_efs_access_point" { - count = var.install_sso_css_grafana + count = var.install_grafana file_system_id = aws_efs_file_system.efs_sso_grafana[count.index].id root_directory { @@ -52,7 +52,7 @@ resource "aws_efs_access_point" "sso_grafana_efs_access_point" { { Name = "sso-grafana-data" }, - var.sso_grafana_tags + var.grafana_tags ) } diff --git a/terraform/lambda-app.tf b/terraform/lambda-app.tf index 16e4516a6..71e7be7f5 100644 --- a/terraform/lambda-app.tf +++ b/terraform/lambda-app.tf @@ -62,9 +62,12 @@ resource "aws_lambda_function" "app" { BCSC_REGISTRATION_BASE_URL_DEV = var.bcsc_registration_base_url_dev BCSC_REGISTRATION_BASE_URL_TEST = var.bcsc_registration_base_url_test BCSC_REGISTRATION_BASE_URL_PROD = var.bcsc_registration_base_url_prod + REDIS_HOST = var.install_redis == 1 ? aws_lb.redis_nlb[0].dns_name : "" } } + depends_on = [aws_lb.redis_nlb] + timeout = 30 # up to 900 seconds (15 minutes) memory_size = 240 # 128 MB to 10,240 MB, in 1-MB increments ephemeral_storage { diff --git a/terraform/lambda-css-api.tf b/terraform/lambda-css-api.tf index f5bdeca86..df58de869 100644 --- a/terraform/lambda-css-api.tf +++ b/terraform/lambda-css-api.tf @@ -46,6 +46,9 @@ resource "aws_lambda_function" "css_api" { CHES_PASSWORD = var.ches_password CHES_USERNAME = var.ches_username GOLD_IP_ADDRESS = var.gold_ip_address + REDIS_HOST = var.install_redis == 1 ? aws_lb.redis_nlb[0].dns_name : "" + GRAFANA_API_TOKEN = var.grafana_api_token + GRAFANA_API_URL = var.grafana_api_url } } diff --git a/terraform/lambda-request-queue.tf b/terraform/lambda-request-queue.tf index 3c7209613..41ac26c29 100644 --- a/terraform/lambda-request-queue.tf +++ b/terraform/lambda-request-queue.tf @@ -19,7 +19,7 @@ resource "aws_lambda_function" "request_queue" { environment { variables = { - NODE_ENV = "production" + NODE_ENV = var.app_env DB_HOSTNAME = module.db.this_rds_cluster_endpoint DB_USERNAME = var.db_username DB_PASSWORD = random_password.db_password.result @@ -46,6 +46,7 @@ resource "aws_lambda_function" "request_queue" { BCSC_REGISTRATION_BASE_URL_DEV = var.bcsc_registration_base_url_dev BCSC_REGISTRATION_BASE_URL_TEST = var.bcsc_registration_base_url_test BCSC_REGISTRATION_BASE_URL_PROD = var.bcsc_registration_base_url_prod + RC_WEBHOOK = var.rc_webhook } } diff --git a/terraform/main.tf b/terraform/main.tf index 5877c69db..70099c36c 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -65,7 +65,7 @@ data "aws_security_group" "web" { name = "Web_sg" } -# resource "aws_security_group" "ecs_sso_grafana_sg" { +# resource "aws_security_group" "grafana" { # name = "ecs-sso-grafana-sg" # description = "Allow inbound access from the ALB only" # vpc_id = module.network.aws_vpc.id @@ -73,8 +73,8 @@ data "aws_security_group" "web" { # ingress { # description = "Only from alb" # protocol = "tcp" -# from_port = var.sso_grafana_container_port -# to_port = var.sso_grafana_container_port +# from_port = 3000 +# to_port = 3000 # security_groups = [data.aws_security_group.app.id] # } @@ -86,5 +86,5 @@ data "aws_security_group" "web" { # cidr_blocks = ["0.0.0.0/0"] # } -# tags = var.sso_grafana_tags +# tags = var.grafana_tags # } diff --git a/terraform/nlb.tf b/terraform/nlb.tf new file mode 100644 index 000000000..5d501ee2e --- /dev/null +++ b/terraform/nlb.tf @@ -0,0 +1,38 @@ +resource "aws_lb" "redis_nlb" { + count = var.install_redis + name = "redis-nlb" + internal = true + load_balancer_type = "network" + security_groups = [data.aws_security_group.app.id, aws_security_group.rds_sg.id] + subnets = [data.aws_subnet.a.id, data.aws_subnet.b.id] +} + +resource "aws_lb_target_group" "redis" { + count = var.install_redis + name = "redis" + port = 6379 + protocol = "TCP" + vpc_id = data.aws_vpc.selected.id + target_type = "ip" + + health_check { + healthy_threshold = 3 + unhealthy_threshold = 3 + timeout = 5 + interval = 30 + protocol = "TCP" + } + tags = var.redis_tags +} + +resource "aws_lb_listener" "redis" { + count = var.install_redis + load_balancer_arn = aws_lb.redis_nlb[count.index].arn + port = 6379 + protocol = "TCP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.redis[count.index].arn + } +} diff --git a/terraform/roles.tf b/terraform/roles.tf index 70df71943..66da4de9e 100644 --- a/terraform/roles.tf +++ b/terraform/roles.tf @@ -1,6 +1,5 @@ # ECS task execution role data -data "aws_iam_policy_document" "ecs_sso_grafana_task_execution_role" { - count = var.install_sso_css_grafana +data "aws_iam_policy_document" "ecs_task_execution_role" { version = "2012-10-17" statement { sid = "" @@ -14,35 +13,37 @@ data "aws_iam_policy_document" "ecs_sso_grafana_task_execution_role" { } } -data "aws_iam_policy" "iam_sso_grafana_read_secret_policy" { - count = var.install_sso_css_grafana +data "aws_iam_policy" "grafana_read_secret" { + count = var.install_grafana name = "SSOPathfinderReadGrafanaSecret" } -# ECS task execution role -resource "aws_iam_role" "ecs_sso_grafana_task_execution_role" { - count = var.install_sso_css_grafana - name = "SSODefaultECSTaskExecutionRole" - assume_role_policy = data.aws_iam_policy_document.ecs_sso_grafana_task_execution_role[0].json +# Grafana ECS task execution role +resource "aws_iam_role" "grafana_task_execution_role" { + count = var.install_grafana + name = "GrafanaECSTaskExecutionRole" + assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_role.json - tags = var.sso_grafana_tags + tags = var.grafana_tags } + + # Attaching task execution and read from RDS policies to task execution role -resource "aws_iam_role_policy_attachment" "ecs_sso_grafana_task_role_policy_attachment" { - role = aws_iam_role.ecs_sso_grafana_task_execution_role[0].name - for_each = var.install_sso_css_grafana == 1 ? toset([ +resource "aws_iam_role_policy_attachment" "grafana" { + role = aws_iam_role.grafana_task_execution_role[0].name + for_each = var.install_grafana == 1 ? toset([ "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", "arn:aws:iam::aws:policy/AmazonRDSReadOnlyAccess", - data.aws_iam_policy.iam_sso_grafana_read_secret_policy[0].arn # secret and policy manually created in AWS + data.aws_iam_policy.grafana_read_secret[0].arn # secret and policy manually created in AWS ]) : toset([]) policy_arn = each.value } -resource "aws_iam_role_policy" "ecs_sso_grafana_task_execution_cwlogs" { - count = var.install_sso_css_grafana - name = "ecs-sso-grafana-task-exec-cwlogs" - role = aws_iam_role.ecs_sso_grafana_task_execution_role[0].id +resource "aws_iam_role_policy" "grafana_task_execution_cwlogs" { + count = var.install_grafana + name = "grafana-task-exec-cwlogs" + role = aws_iam_role.grafana_task_execution_role[0].id policy = <<-EOF { @@ -62,9 +63,9 @@ resource "aws_iam_role_policy" "ecs_sso_grafana_task_execution_cwlogs" { EOF } -resource "aws_iam_role" "sso_grafana_container_role" { - count = var.install_sso_css_grafana - name = "sso-grafana-container-role" +resource "aws_iam_role" "grafana_container_role" { + count = var.install_grafana + name = "grafana-container-role" assume_role_policy = <