diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index d08f6729d..05dc69ab3 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -2,25 +2,36 @@ name: Integration Testing on: workflow_dispatch: + # Integration tests are slow, so only run them if relevant files have changed. + # This is done at the workflow level and at the job level. + # Make sure these triggers stay consistent with the 'changed-files' job. push: branches: - - "main" + - 'main' + - 'rc' paths: - ".github/workflows/integration-test.yml" # this file - - "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment - - "./utils/**/*" # any change to test fixtures - - "./docker-compose.yml" # any change to Docker configuration - - "./package.json" # dependencies + - "docker-compose.yml" # any change to Docker configuration + - "package.json" # dependencies + - "utils/**" # any change to test fixtures + - "src/services/sync.service.ts" # core sync service used by all directory services + - "src/services/directory-services/ldap-directory.service*" # LDAP directory service + - "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service + # Add directory services here as we add test coverage pull_request: paths: - ".github/workflows/integration-test.yml" # this file - - "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment - - "./utils/**/*" # any change to test fixtures - - "./docker-compose.yml" # any change to Docker configuration - - "./package.json" # dependencies + - "docker-compose.yml" # any change to Docker configuration + - "package.json" # dependencies + - "utils/**" # any change to test fixtures + - "src/services/sync.service.ts" # core sync service used by all directory services + - "src/services/directory-services/ldap-directory.service*" # LDAP directory service + - "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service + # Add directory services here as we add test coverage permissions: contents: read checks: write # required by dorny/test-reporter to upload its results + id-token: write # required to use OIDC to login to Azure Key Vault jobs: testing: name: Run tests @@ -50,23 +61,79 @@ jobs: - name: Install Node dependencies run: npm ci - - name: Install mkcert + # Get secrets from Azure Key Vault + - name: Azure Login + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get KV Secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-directory-connector + secrets: "GOOGLE-ADMIN-USER,GOOGLE-CLIENT-EMAIL,GOOGLE-DOMAIN,GOOGLE-PRIVATE-KEY" + + - name: Azure Logout + uses: bitwarden/gh-actions/azure-logout@main + + # Only run relevant tests depending on what files have changed. + # This should be kept consistent with the workflow level triggers. + # Note: docker-compose.yml is only used for ldap for now + - name: Get changed files + id: changed-files + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + list-files: shell + token: ${{ secrets.GITHUB_TOKEN }} + # Add directory services here as we add test coverage + filters: | + common: + - '.github/workflows/integration-test.yml' + - 'utils/**' + - 'package.json' + - 'src/services/sync.service.ts' + ldap: + - 'docker-compose.yml' + - 'src/services/directory-services/ldap-directory.service*' + google: + - 'src/services/directory-services/gsuite-directory.service*' + + # LDAP + - name: Setup LDAP integration tests + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true' run: | sudo apt-get update sudo apt-get -y install mkcert - - - name: Setup LDAP integration tests - run: npm run test:integration:setup + npm run test:integration:setup - name: Run LDAP integration tests - run: npx jest ldap-directory.service.integration.spec.ts --coverage + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true' + env: + JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing + run: npx jest ldap-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-ldap + + # Google Workspace + - name: Run Google Workspace integration tests + if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.google == 'true' + env: + GOOGLE_DOMAIN: ${{ steps.get-kv-secrets.outputs.GOOGLE-DOMAIN }} + GOOGLE_ADMIN_USER: ${{ steps.get-kv-secrets.outputs.GOOGLE-ADMIN-USER }} + GOOGLE_CLIENT_EMAIL: ${{ steps.get-kv-secrets.outputs.GOOGLE-CLIENT-EMAIL }} + GOOGLE_PRIVATE_KEY: ${{ steps.get-kv-secrets.outputs.GOOGLE-PRIVATE-KEY }} + JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing + run: | + npx jest gsuite-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-google - name: Report test results + id: report uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1 - if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} + if: github.event.pull_request.head.repo.full_name == github.repository && !cancelled() with: name: Test Results - path: "junit.xml" + path: "junit.xml*" reporter: jest-junit fail-on-error: true diff --git a/.gitignore b/.gitignore index 69cc709de..295b79280 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,8 @@ build-cli .angular/cache # Testing -coverage -junit.xml +coverage* +junit.xml* # Misc *.crx diff --git a/src/abstractions/directory-factory.service.ts b/src/abstractions/directory-factory.service.ts index 90d2f6743..2f775afce 100644 --- a/src/abstractions/directory-factory.service.ts +++ b/src/abstractions/directory-factory.service.ts @@ -1,5 +1,5 @@ import { DirectoryType } from "@/src/enums/directoryType"; -import { IDirectoryService } from "@/src/services/directory.service"; +import { IDirectoryService } from "@/src/services/directory-services/directory.service"; export abstract class DirectoryFactoryService { abstract createService(type: DirectoryType): IDirectoryService; diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index b8859b1b6..272991f37 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -768,5 +768,8 @@ }, "launchWebVault": { "message": "Launch Web Vault" + }, + "authenticationFailed": { + "message": "Authentication failed" } } diff --git a/src/services/directory-factory.service.ts b/src/services/directory-factory.service.ts index e3da3c043..eccea5c22 100644 --- a/src/services/directory-factory.service.ts +++ b/src/services/directory-factory.service.ts @@ -5,11 +5,11 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi import { StateService } from "../abstractions/state.service"; import { DirectoryType } from "../enums/directoryType"; -import { EntraIdDirectoryService } from "./entra-id-directory.service"; -import { GSuiteDirectoryService } from "./gsuite-directory.service"; -import { LdapDirectoryService } from "./ldap-directory.service"; -import { OktaDirectoryService } from "./okta-directory.service"; -import { OneLoginDirectoryService } from "./onelogin-directory.service"; +import { EntraIdDirectoryService } from "./directory-services/entra-id-directory.service"; +import { GSuiteDirectoryService } from "./directory-services/gsuite-directory.service"; +import { LdapDirectoryService } from "./directory-services/ldap-directory.service"; +import { OktaDirectoryService } from "./directory-services/okta-directory.service"; +import { OneLoginDirectoryService } from "./directory-services/onelogin-directory.service"; export class DefaultDirectoryFactoryService implements DirectoryFactoryService { constructor( diff --git a/src/services/directory.service.ts b/src/services/directory-services/directory.service.ts similarity index 53% rename from src/services/directory.service.ts rename to src/services/directory-services/directory.service.ts index e13f87322..d7937e413 100644 --- a/src/services/directory.service.ts +++ b/src/services/directory-services/directory.service.ts @@ -1,5 +1,5 @@ -import { GroupEntry } from "../models/groupEntry"; -import { UserEntry } from "../models/userEntry"; +import { GroupEntry } from "../../models/groupEntry"; +import { UserEntry } from "../../models/userEntry"; export interface IDirectoryService { getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>; diff --git a/src/services/entra-id-directory.service.ts b/src/services/directory-services/entra-id-directory.service.ts similarity index 97% rename from src/services/entra-id-directory.service.ts rename to src/services/directory-services/entra-id-directory.service.ts index 449e11b5c..d76f57a54 100644 --- a/src/services/entra-id-directory.service.ts +++ b/src/services/directory-services/entra-id-directory.service.ts @@ -7,14 +7,14 @@ import * as graphType from "@microsoft/microsoft-graph-types"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { EntraIdConfiguration } from "../models/entraIdConfiguration"; -import { GroupEntry } from "../models/groupEntry"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; - -import { BaseDirectoryService } from "./baseDirectory.service"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { EntraIdConfiguration } from "../../models/entraIdConfiguration"; +import { GroupEntry } from "../../models/groupEntry"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; +import { BaseDirectoryService } from "../baseDirectory.service"; + import { IDirectoryService } from "./directory.service"; const EntraIdPublicIdentityAuthority = "login.microsoftonline.com"; diff --git a/src/services/gsuite-directory.service.integration.spec.ts b/src/services/directory-services/gsuite-directory.service.integration.spec.ts similarity index 80% rename from src/services/gsuite-directory.service.integration.spec.ts rename to src/services/directory-services/gsuite-directory.service.integration.spec.ts index c58de9575..397e594c9 100644 --- a/src/services/gsuite-directory.service.integration.spec.ts +++ b/src/services/directory-services/gsuite-directory.service.integration.spec.ts @@ -1,18 +1,18 @@ import { config as dotenvConfig } from "dotenv"; import { mock, MockProxy } from "jest-mock-extended"; -import { I18nService } from "../../jslib/common/src/abstractions/i18n.service"; -import { LogService } from "../../jslib/common/src/abstractions/log.service"; +import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service"; +import { LogService } from "../../../jslib/common/src/abstractions/log.service"; import { getGSuiteConfiguration, getSyncConfiguration, -} from "../../utils/google-workspace/config-fixtures"; -import { groupFixtures } from "../../utils/google-workspace/group-fixtures"; -import { userFixtures } from "../../utils/google-workspace/user-fixtures"; -import { DirectoryType } from "../enums/directoryType"; +} from "../../../utils/google-workspace/config-fixtures"; +import { groupFixtures } from "../../../utils/google-workspace/group-fixtures"; +import { userFixtures } from "../../../utils/google-workspace/user-fixtures"; +import { DirectoryType } from "../../enums/directoryType"; +import { StateService } from "../state.service"; import { GSuiteDirectoryService } from "./gsuite-directory.service"; -import { StateService } from "./state.service"; // These tests integrate with a test Google Workspace instance. // Credentials are located in the shared Bitwarden collection for Directory Connector testing. @@ -24,10 +24,13 @@ dotenvConfig({ path: "utils/.env" }); // These filters target integration test data. // These should return data that matches the user and group fixtures exactly. // There may be additional data present if not used. -const INTEGRATION_USER_FILTER = - "exclude:integration-user-a@bwrox.dev|orgUnitPath='/Integration testing'"; +const INTEGRATION_USER_FILTER = "|orgUnitPath='/Integration testing'"; const INTEGRATION_GROUP_FILTER = "|name:Integration*"; +// These tests are slow! +// Increase the default timeout from 5s to 15s +jest.setTimeout(15000); + describe("gsuiteDirectoryService", () => { let logService: MockProxy; let i18nService: MockProxy; diff --git a/src/services/gsuite-directory.service.ts b/src/services/directory-services/gsuite-directory.service.ts similarity index 90% rename from src/services/gsuite-directory.service.ts rename to src/services/directory-services/gsuite-directory.service.ts index 714818ebc..95714fb4f 100644 --- a/src/services/gsuite-directory.service.ts +++ b/src/services/directory-services/gsuite-directory.service.ts @@ -4,14 +4,14 @@ import { admin_directory_v1, google } from "googleapis"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from "../models/groupEntry"; -import { GSuiteConfiguration } from "../models/gsuiteConfiguration"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; - -import { BaseDirectoryService } from "./baseDirectory.service"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { GroupEntry } from "../../models/groupEntry"; +import { GSuiteConfiguration } from "../../models/gsuiteConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; +import { BaseDirectoryService } from "../baseDirectory.service"; + import { IDirectoryService } from "./directory.service"; export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService { @@ -253,7 +253,15 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir ], }); - await this.client.authorize(); + try { + await this.client.authorize(); + } catch (error) { + // Catch and rethrow this to sanitize any sensitive info (e.g. private key) in the error message + this.logService.error( + `Google Workspace authentication failed: ${error?.name || "Unknown error"}`, + ); + throw new Error(this.i18nService.t("authenticationFailed")); + } this.authParams = { auth: this.client, diff --git a/src/services/ldap-directory.service.integration.spec.ts b/src/services/directory-services/ldap-directory.service.integration.spec.ts similarity index 91% rename from src/services/ldap-directory.service.integration.spec.ts rename to src/services/directory-services/ldap-directory.service.integration.spec.ts index c780f4c65..67d1be55d 100644 --- a/src/services/ldap-directory.service.integration.spec.ts +++ b/src/services/directory-services/ldap-directory.service.integration.spec.ts @@ -1,14 +1,17 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { I18nService } from "../../jslib/common/src/abstractions/i18n.service"; -import { LogService } from "../../jslib/common/src/abstractions/log.service"; -import { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures"; -import { groupFixtures } from "../../utils/openldap/group-fixtures"; -import { userFixtures } from "../../utils/openldap/user-fixtures"; -import { DirectoryType } from "../enums/directoryType"; +import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service"; +import { LogService } from "../../../jslib/common/src/abstractions/log.service"; +import { + getLdapConfiguration, + getSyncConfiguration, +} from "../../../utils/openldap/config-fixtures"; +import { groupFixtures } from "../../../utils/openldap/group-fixtures"; +import { userFixtures } from "../../../utils/openldap/user-fixtures"; +import { DirectoryType } from "../../enums/directoryType"; +import { StateService } from "../state.service"; import { LdapDirectoryService } from "./ldap-directory.service"; -import { StateService } from "./state.service"; // These tests integrate with the OpenLDAP docker image and seed data located in the openldap folder. // To run theses tests: diff --git a/src/services/ldap-directory.service.ts b/src/services/directory-services/ldap-directory.service.ts similarity index 97% rename from src/services/ldap-directory.service.ts rename to src/services/directory-services/ldap-directory.service.ts index b08f0a085..b9893885c 100644 --- a/src/services/ldap-directory.service.ts +++ b/src/services/directory-services/ldap-directory.service.ts @@ -7,12 +7,12 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; import { Utils } from "@/jslib/common/src/misc/utils"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from "../models/groupEntry"; -import { LdapConfiguration } from "../models/ldapConfiguration"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { GroupEntry } from "../../models/groupEntry"; +import { LdapConfiguration } from "../../models/ldapConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; import { IDirectoryService } from "./directory.service"; diff --git a/src/services/okta-directory.service.ts b/src/services/directory-services/okta-directory.service.ts similarity index 95% rename from src/services/okta-directory.service.ts rename to src/services/directory-services/okta-directory.service.ts index 2c2d99130..c73fd49a0 100644 --- a/src/services/okta-directory.service.ts +++ b/src/services/directory-services/okta-directory.service.ts @@ -3,14 +3,14 @@ import * as https from "https"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from "../models/groupEntry"; -import { OktaConfiguration } from "../models/oktaConfiguration"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; - -import { BaseDirectoryService } from "./baseDirectory.service"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { GroupEntry } from "../../models/groupEntry"; +import { OktaConfiguration } from "../../models/oktaConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; +import { BaseDirectoryService } from "../baseDirectory.service"; + import { IDirectoryService } from "./directory.service"; const DelayBetweenBuildGroupCallsInMilliseconds = 500; diff --git a/src/services/onelogin-directory.service.ts b/src/services/directory-services/onelogin-directory.service.ts similarity index 93% rename from src/services/onelogin-directory.service.ts rename to src/services/directory-services/onelogin-directory.service.ts index d46398c97..9b8eb2a2d 100644 --- a/src/services/onelogin-directory.service.ts +++ b/src/services/directory-services/onelogin-directory.service.ts @@ -1,14 +1,14 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "../abstractions/state.service"; -import { DirectoryType } from "../enums/directoryType"; -import { GroupEntry } from "../models/groupEntry"; -import { OneLoginConfiguration } from "../models/oneLoginConfiguration"; -import { SyncConfiguration } from "../models/syncConfiguration"; -import { UserEntry } from "../models/userEntry"; - -import { BaseDirectoryService } from "./baseDirectory.service"; +import { StateService } from "../../abstractions/state.service"; +import { DirectoryType } from "../../enums/directoryType"; +import { GroupEntry } from "../../models/groupEntry"; +import { OneLoginConfiguration } from "../../models/oneLoginConfiguration"; +import { SyncConfiguration } from "../../models/syncConfiguration"; +import { UserEntry } from "../../models/userEntry"; +import { BaseDirectoryService } from "../baseDirectory.service"; + import { IDirectoryService } from "./directory.service"; // Basic email validation: something@something.something diff --git a/src/services/sync.service.integration.spec.ts b/src/services/sync.service.integration.spec.ts index e6ff27806..241d5f0b0 100644 --- a/src/services/sync.service.integration.spec.ts +++ b/src/services/sync.service.integration.spec.ts @@ -12,7 +12,7 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi import { DirectoryType } from "../enums/directoryType"; import { BatchRequestBuilder } from "./batch-request-builder"; -import { LdapDirectoryService } from "./ldap-directory.service"; +import { LdapDirectoryService } from "./directory-services/ldap-directory.service"; import { SingleRequestBuilder } from "./single-request-builder"; import { StateService } from "./state.service"; import { SyncService } from "./sync.service"; diff --git a/src/services/sync.service.spec.ts b/src/services/sync.service.spec.ts index 726e6240f..46a111db5 100644 --- a/src/services/sync.service.spec.ts +++ b/src/services/sync.service.spec.ts @@ -11,8 +11,8 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi import { DirectoryType } from "../enums/directoryType"; import { BatchRequestBuilder } from "./batch-request-builder"; +import { LdapDirectoryService } from "./directory-services/ldap-directory.service"; import { I18nService } from "./i18n.service"; -import { LdapDirectoryService } from "./ldap-directory.service"; import { SingleRequestBuilder } from "./single-request-builder"; import { StateService } from "./state.service"; import { SyncService } from "./sync.service";