From 225df0565d29e7c44038e99e6e150e8c383016b0 Mon Sep 17 00:00:00 2001 From: Alessandro Barbarossa Date: Tue, 3 Dec 2024 13:08:22 +0100 Subject: [PATCH] test: enable auth providers for nightly jobs (#1923) * enable auth providers for nightly jobs * enable auth providers for nightly jobs * enable auth providers for nightly jobs * enable auth providers for nightly jobs * enable auth providers for nightly jobs * fix error * fix error * fix error * fixes * fix helm delete * fix login timeout * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix timeouts * fix gh test * add config for nightly job * fix lint errors * fix lint errors --- .ibm/pipelines/env_variables.sh | 29 + .ibm/pipelines/openshift-ci-tests.sh | 8 +- ...lues_showcase-auth-provider-diff-rhbk.yaml | 24 + .../values_showcase-auth-providers.yaml | 11 +- e2e-tests/playwright.config.ts | 7 +- .../basic-authentication.spec.ts | 29 +- .../e2e/authProviders/github-provider.spec.ts | 382 +++-- .../authProviders/microsoft-provider.spec.ts | 519 ++++--- .../authProviders/rhsso-76-provider.spec.ts | 1357 +++++++++-------- .../authProviders/setup-environment.spec.ts | 5 +- e2e-tests/playwright/utils/api-helper.ts | 141 ++ .../authenticationProviders/constants.ts | 111 +- .../authenticationProviders/github-helper.ts | 51 +- .../authenticationProviders/msgraph-helper.ts | 4 + .../authenticationProviders/rh-sso-helper.ts | 398 ++--- e2e-tests/playwright/utils/common.ts | 268 ++-- e2e-tests/playwright/utils/helm.ts | 2 +- e2e-tests/playwright/utils/helper.ts | 189 ++- e2e-tests/playwright/utils/kube-client.ts | 1 - e2e-tests/playwright/utils/logger.ts | 2 +- e2e-tests/playwright/utils/ui-helper.ts | 32 +- test-results/.last-run.json | 4 + 22 files changed, 2194 insertions(+), 1380 deletions(-) create mode 100644 .ibm/pipelines/value_files/values_showcase-auth-provider-diff-rhbk.yaml create mode 100644 test-results/.last-run.json diff --git a/.ibm/pipelines/env_variables.sh b/.ibm/pipelines/env_variables.sh index 53d9c434b2..41346c3344 100755 --- a/.ibm/pipelines/env_variables.sh +++ b/.ibm/pipelines/env_variables.sh @@ -107,4 +107,33 @@ GKE_SERVICE_ACCOUNT_NAME=$(cat /tmp/secrets/GKE_SERVICE_ACCOUNT_NAME) GKE_CERT_NAME=$(cat /tmp/secrets/GKE_CERT_NAME) GOOGLE_CLOUD_PROJECT=$(cat /tmp/secrets/GOOGLE_CLOUD_PROJECT) +# authentication providers variables +RHSSO76_ADMIN_USERNAME=$(cat /tmp/secrets/RHSSO76_ADMIN_USERNAME) +RHSSO76_ADMIN_PASSWORD=$(cat /tmp/secrets/RHSSO76_ADMIN_PASSWORD) +RHSSO76_DEFAULT_PASSWORD=$(cat /tmp/secrets/RHSSO76_DEFAULT_PASSWORD) +RHSSO76_URL=$(cat /tmp/secrets/RHSSO76_URL) +RHSSO76_CLIENT_SECRET=$(cat /tmp/secrets/RHSSO76_CLIENT_SECRET) +RHSSO76_CLIENT_ID="myclient" +AUTH_PROVIDERS_REALM_NAME="authProviders" + +AZURE_LOGIN_USERNAME=$(cat /tmp/secrets/AZURE_LOGIN_USERNAME) +AZURE_LOGIN_PASSWORD=$(cat /tmp/secrets/AZURE_LOGIN_PASSWORD) +AUTH_PROVIDERS_AZURE_CLIENT_ID=$(cat /tmp/secrets/AUTH_PROVIDERS_AZURE_CLIENT_ID) +AUTH_PROVIDERS_AZURE_CLIENT_SECRET=$(cat /tmp/secrets/AUTH_PROVIDERS_AZURE_CLIENT_SECRET) +AUTH_PROVIDERS_AZURE_TENANT_ID=$(cat /tmp/secrets/AUTH_PROVIDERS_AZURE_TENANT_ID) + +AUTH_PROVIDERS_GH_ORG_NAME="rhdhqeauthorg" +AUTH_ORG_APP_ID=$(cat /tmp/secrets/AUTH_ORG_APP_ID) +AUTH_ORG_CLIENT_ID=$(cat /tmp/secrets/AUTH_ORG_CLIENT_ID) +AUTH_ORG_CLIENT_SECRET=$(cat /tmp/secrets/AUTH_ORG_CLIENT_SECRET) +AUTH_ORG1_PRIVATE_KEY=$(cat /tmp/secrets/AUTH_ORG1_PRIVATE_KEY) +AUTH_ORG_PK=$(cat /tmp/secrets/AUTH_ORG_PK) +AUTH_ORG_WEBHOOK_SECRET=$(cat /tmp/secrets/AUTH_ORG_WEBHOOK_SECRET) +GH_USER_PASSWORD=$(cat /tmp/secrets/GH_USER_PASSWORD) + +AUTH_PROVIDERS_RELEASE="rhdh-auth-providers" +AUTH_PROVIDERS_NAMESPACE="showcase-auth-providers" +STATIC_API_TOKEN="somecicdtoken" +AUTH_PROVIDERS_CHART="rhdh-chart/backstage" + set +a # Stop automatically exporting variables diff --git a/.ibm/pipelines/openshift-ci-tests.sh b/.ibm/pipelines/openshift-ci-tests.sh index 6b9a90294f..d33b1f3d52 100755 --- a/.ibm/pipelines/openshift-ci-tests.sh +++ b/.ibm/pipelines/openshift-ci-tests.sh @@ -282,7 +282,11 @@ run_tests() { cp -a /tmp/backstage-showcase/e2e-tests/${JUNIT_RESULTS} "${ARTIFACT_DIR}/${project}/${JUNIT_RESULTS}" if [ -d "/tmp/backstage-showcase/e2e-tests/screenshots" ]; then - cp -a /tmp/backstage-showcase/e2e-tests/screenshots/* "${ARTIFACT_DIR}/${project}/attachments/screenshots/" + cp -a /tmp/backstage-showcase/e2e-tests/screenshots/* "${ARTIFACT_DIR}/${project}/attachments/screenshots/" + fi + + if [ -d "/tmp/backstage-showcase/e2e-tests/auth-providers-logs" ]; then + cp -a /tmp/backstage-showcase/e2e-tests/auth-providers-logs/* "${ARTIFACT_DIR}/${project}/" fi ansi2html <"/tmp/${LOGFILE}" >"/tmp/${LOGFILE}.html" @@ -487,6 +491,8 @@ main() { initiate_rbac_gke_deployment check_and_test "${RELEASE_NAME_RBAC}" "${NAME_SPACE_RBAC_K8S}" delete_namespace "${NAME_SPACE_RBAC_K8S}" + elif [[ "$JOB_NAME" == *auth-providers* ]]; then + run_tests "${AUTH_PROVIDERS_RELEASE}" "${AUTH_PROVIDERS_NAMESPACE}" else initiate_deployments check_and_test "${RELEASE_NAME}" "${NAME_SPACE}" diff --git a/.ibm/pipelines/value_files/values_showcase-auth-provider-diff-rhbk.yaml b/.ibm/pipelines/value_files/values_showcase-auth-provider-diff-rhbk.yaml new file mode 100644 index 0000000000..b96d9a951b --- /dev/null +++ b/.ibm/pipelines/value_files/values_showcase-auth-provider-diff-rhbk.yaml @@ -0,0 +1,24 @@ +upstream: + backstage: + appConfig: + auth: + providers: + oidc: + production: + metadataUrl: ${RHBK_METADATA_URL} + clientId: ${RHBK_CLIENT_ID} + clientSecret: ${RHBK_CLIENT_SECRET} + prompt: auto + callbackUrl: ${RHBK_CALLBACK_URL} + catalog: + providers: + keycloakOrg: + default: + baseUrl: ${RHBK_URL} + loginRealm: ${AUTH_PROVIDERS_REALM_NAME} + realm: ${AUTH_PROVIDERS_REALM_NAME} + clientId: ${RHBK_CLIENT_ID} + clientSecret: ${RHBK_CLIENT_SECRET} + schedule: + frequency: { minutes: 1 } + timeout: { minutes: 1 } \ No newline at end of file diff --git a/.ibm/pipelines/value_files/values_showcase-auth-providers.yaml b/.ibm/pipelines/value_files/values_showcase-auth-providers.yaml index 1c1be077d7..b032881d07 100644 --- a/.ibm/pipelines/value_files/values_showcase-auth-providers.yaml +++ b/.ibm/pipelines/value_files/values_showcase-auth-providers.yaml @@ -65,7 +65,7 @@ upstream: githubUrl: https://github.com orgs: ['${AUTH_PROVIDERS_GH_ORG_NAME}'] schedule: - initialDelay: { seconds: 10 } + initialDelay: { seconds: 0 } frequency: { minutes: 1 } timeout: { minutes: 1 } microsoftGraphOrg: @@ -105,12 +105,19 @@ upstream: - name: user:default/qeadmin_rhdhtesting.onmicrosoft.com - name: user:default/rhsso_admin - name: user:default/rhdhqeauthadmin + backend: + auth: + externalAccess: + - type: static + options: + token: "somecicdtoken" + subject: e2e-tests-ci extraEnvVarsSecrets: - rhdh-secrets image: registry: quay.io repository: rhdh/rhdh-hub-rhel9 - tag: '1.3' + tag: 'next' readinessProbe: failureThreshold: 3 httpGet: diff --git a/e2e-tests/playwright.config.ts b/e2e-tests/playwright.config.ts index d44d24f9ea..6c880fa068 100644 --- a/e2e-tests/playwright.config.ts +++ b/e2e-tests/playwright.config.ts @@ -67,10 +67,7 @@ export default defineConfig({ }, { name: "showcase-auth-providers", - use: { - ...devices["Desktop Chrome"], - viewport: { width: 1920, height: 1080 }, - }, + ...useCommonDeviceAndViewportConfig, testMatch: ["**/playwright/e2e/authProviders/*.spec.ts"], testIgnore: [ "**/playwright/e2e/authProviders/setup-environment.spec.ts", @@ -79,7 +76,7 @@ export default defineConfig({ ], dependencies: ["showcase-auth-providers-setup-environment"], teardown: "showcase-auth-providers-clear-environment", - retries: 2, + retries: 1, }, { name: "showcase-auth-providers-setup-environment", diff --git a/e2e-tests/playwright/e2e/authProviders/basic-authentication.spec.ts b/e2e-tests/playwright/e2e/authProviders/basic-authentication.spec.ts index 59c1a0c00f..4265ce09cd 100644 --- a/e2e-tests/playwright/e2e/authProviders/basic-authentication.spec.ts +++ b/e2e-tests/playwright/e2e/authProviders/basic-authentication.spec.ts @@ -2,6 +2,8 @@ import { test, Page, expect } from "@playwright/test"; import { Common, setupBrowser } from "../../utils/common"; import { UIhelper } from "../../utils/ui-helper"; import * as constants from "../../utils/authenticationProviders/constants"; +import { dumpAllPodsLogs, dumpRHDHUsersAndGroups } from "../../utils/helper"; +import { APIHelper } from "../../utils/api-helper"; import { LOGGER } from "../../utils/logger"; import { HelmActions } from "../../utils/helm"; @@ -39,16 +41,16 @@ test.describe("Standard authentication providers: Basic authentication", () => { constants.QUAY_REPO, constants.TAG_NAME, [ - "--set upstream.backstage.appConfig.auth.providers=null", + "--set upstream.backstage.appConfig.auth.providers.guest.dangerouslyAllowOutsideDevelopment=false", "--set upstream.backstage.appConfig.auth.environment=development", "--set upstream.backstage.appConfig.catalog.providers=null", + "--set upstream.backstage.appConfig.permission.enabled=false", ], ); // Guest login should work await common.loginAsGuest(); - await uiHelper.openSidebar("Settings"); - await uiHelper.verifyHeading("Guest"); + await page.goto("/"); await uiHelper.openSidebar("Settings"); await common.signOut(); }); @@ -77,6 +79,7 @@ test.describe("Standard authentication providers: Basic authentication", () => { "--set upstream.backstage.appConfig.auth.environment=development", "--set upstream.backstage.appConfig.signInPage=microsoft", "--set upstream.backstage.appConfig.catalog.providers=null", + "--set upstream.backstage.appConfig.permission.enabled=false", ], ); @@ -112,6 +115,7 @@ test.describe("Standard authentication providers: Basic authentication", () => { "--set upstream.backstage.appConfig.signInPage=microsoft", "--set upstream.backstage.appConfig.dangerouslyAllowSignInWithoutUserInCatalog=true", "--set upstream.backstage.appConfig.catalog.providers=null", + "--set upstream.backstage.appConfig.permission.enabled=false", ], ); @@ -124,10 +128,10 @@ test.describe("Standard authentication providers: Basic authentication", () => { await uiHelper.verifyParagraph(constants.AZURE_LOGIN_USERNAME); // check no entities are in the catalog - await page.goto("/catalog?filters[kind]=user&filters[user]=all"); - await uiHelper.verifyHeading("My Org Catalog"); - await uiHelper.searchInputPlaceholder(constants.AZURE_LOGIN_FIRSTNAME); - await uiHelper.verifyRowsInTable(["No records to display"]); + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); + const catalogUsers = await api.getAllCatalogUsersFromAPI(); + expect(catalogUsers.totalItems).toBe(0); await uiHelper.openSidebar("Settings"); await common.signOut(); }); @@ -154,6 +158,7 @@ test.describe("Standard authentication providers: Basic authentication", () => { "--set upstream.backstage.appConfig.signInPage=microsoft", "--set upstream.backstage.appConfig.dangerouslyAllowSignInWithoutUserInCatalog=true", "--set upstream.backstage.appConfig.catalog.providers=null", + "--set upstream.backstage.appConfig.permission.enabled=false", ], ); @@ -162,7 +167,15 @@ test.describe("Standard authentication providers: Basic authentication", () => { const singInMethods = await page .locator("div[class^='MuiCardHeader-root']") .allInnerTexts(); - console.log(singInMethods); expect(singInMethods).not.toContain("Guest"); }); + + test.afterEach(async () => { + if (test.info().status !== test.info().expectedStatus) { + const prefix = `${test.info().testId}_${test.info().retry}`; + LOGGER.info(`Dumping logs with prefix ${prefix}`); + await dumpAllPodsLogs(prefix, constants.LOGS_FOLDER); + await dumpRHDHUsersAndGroups(prefix, constants.LOGS_FOLDER); + } + }); }); diff --git a/e2e-tests/playwright/e2e/authProviders/github-provider.spec.ts b/e2e-tests/playwright/e2e/authProviders/github-provider.spec.ts index 4ffebb7cc6..6d88d02d4d 100644 --- a/e2e-tests/playwright/e2e/authProviders/github-provider.spec.ts +++ b/e2e-tests/playwright/e2e/authProviders/github-provider.spec.ts @@ -6,8 +6,16 @@ import { LOGGER } from "../../utils/logger"; import { waitForNextSync, replaceInRBACPolicyFileConfigMap, + parseGroupMemberFromEntity, + parseGroupChildrenFromEntity, + parseGroupParentFromEntity, + dumpAllPodsLogs, + dumpRHDHUsersAndGroups, } from "../../utils/helper"; import { BrowserContext } from "@playwright/test"; +import { APIHelper } from "../../utils/api-helper"; +import { GroupEntity } from "@backstage/catalog-model"; +import { RhdhAuthHack } from "../../support/api/rhdh-auth-hack"; import * as ghHelper from "../../utils/authenticationProviders/github-helper"; import { HelmActions } from "../../utils/helm"; @@ -19,6 +27,7 @@ test.describe("Standard authentication providers: Github Provider", () => { let common: Common; let context: BrowserContext; let uiHelper: UIhelper; + let mustSync = false; const syncTime = 60; test.beforeAll(async ({ browser }, testInfo) => { @@ -28,6 +37,9 @@ test.describe("Standard authentication providers: Github Provider", () => { common = new Common(page); uiHelper = new UIhelper(page); expect(process.env.BASE_URL).not.toBeNull(); + expect(process.env.AUTH_PROVIDERS_GH_USER_2FA).not.toBeNull(); + expect(process.env.AUTH_PROVIDERS_GH_ADMIN_2FA).not.toBeNull(); + LOGGER.info(`Base Url is ${process.env.BASE_URL}`); LOGGER.info( `Starting scenario: Standard authentication providers: Basic authentication: attemp #${testInfo.retry}`, @@ -74,7 +86,7 @@ test.describe("Standard authentication providers: Github Provider", () => { LOGGER.info( "Executing testcase: Github with default resolver: user should login and entity is in the catalog", ); - test.setTimeout(300 * 1000); + test.setTimeout(30 * 1000); if (test.info().retry > 0) { await waitForNextSync("github", syncTime); } @@ -89,117 +101,126 @@ test.describe("Standard authentication providers: Github Provider", () => { await common.githubLogin( constants.GH_USERS["admin"].name, constants.GH_USER_PASSWORD, + constants.AUTH_PROVIDERS_GH_ADMIN_2FA, ); - await page.goto("/"); - await uiHelper.openSidebar("Catalog"); - await page.reload(); - await uiHelper.selectMuiBox("Kind", "User"); - await uiHelper.verifyHeading("All users"); - await uiHelper.verifyCellsInTable([constants.GH_USERS["user_1"].name]); + await expect(async () => { + expect( + await common.CheckUserIsIngestedInCatalog( + [constants.GH_USERS["user_1"].displayName], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 90 * 1000, + }); + await uiHelper.openSidebar("Settings"); await common.signOut(); }); test("Ingestion of Users and Nested Groups: verify the UserEntities and Groups are created with the correct relationships in RHDH ", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("github", syncTime); - } - await common.githubLogin( - constants.GH_USERS["admin"].name, - constants.GH_USER_PASSWORD, - ); // check entities are in the catalog const usersDisplayNames = Object.values(constants.GH_USERS).map( - (u) => u.name, + (u) => u.displayName, ); - await common.CheckUserIsShowingInCatalog(usersDisplayNames); + expect( + await common.CheckUserIsIngestedInCatalog( + usersDisplayNames, + constants.STATIC_API_TOKEN, + ), + ).toBe(true); // check groups are nested correctly and display all members const groupsDisplayNames = Object.values(constants.GH_TEAMS).map( (g) => g.name, ); - await common.CheckGroupIsShowingInCatalog(groupsDisplayNames); + expect( + await common.CheckGroupIsIngestedInCatalog( + groupsDisplayNames, + constants.STATIC_API_TOKEN, + ), + ).toBe(true); - let displayed; + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); // check team1 - displayed = await common.GoToGroupPageAndGetDisplayedData( + const group1: GroupEntity = await api.getGroupEntityFromAPI( constants.GH_TEAMS["team_1"].name, ); - expect(displayed.groupMembers).toContain(constants.GH_USERS["admin"].name); + const members1 = parseGroupMemberFromEntity(group1); + expect(members1.includes(constants.GH_USERS["admin"].name)).toBe(true); // check team2 - displayed = await common.GoToGroupPageAndGetDisplayedData( + const group2: GroupEntity = await api.getGroupEntityFromAPI( constants.GH_TEAMS["team_2"].name, ); - expect(displayed.groupMembers).toEqual([]); - expect(displayed.childGroups).toContain(constants.GH_TEAMS["team_3"].name); + const members2 = parseGroupMemberFromEntity(group2); + expect(members2).toEqual([]); + + const children2 = parseGroupChildrenFromEntity(group2); + expect(children2.includes(constants.GH_TEAMS["team_3"].name)).toBe(true); // check team3 - displayed = await common.GoToGroupPageAndGetDisplayedData( + const group3: GroupEntity = await api.getGroupEntityFromAPI( constants.GH_TEAMS["team_3"].name, ); - expect(displayed.groupMembers).toContain(constants.GH_USERS["user_1"].name); - expect(displayed.parentGroup).toContain(constants.GH_TEAMS["team_2"].name); + const members3 = parseGroupMemberFromEntity(group3); + expect(members3.includes(constants.GH_USERS["user_1"].name)).toBe(true); + const parent3 = parseGroupParentFromEntity(group3); + expect(parent3.includes(constants.GH_TEAMS["team_2"].name)).toBe(true); // check team4 - displayed = await common.GoToGroupPageAndGetDisplayedData( + const group4: GroupEntity = await api.getGroupEntityFromAPI( constants.GH_TEAMS["team_4"].name, ); - expect(displayed.groupMembers).toContain(constants.GH_USERS["user_1"].name); + const members4 = parseGroupMemberFromEntity(group4); + expect(members4.includes(constants.GH_USERS["user_1"].name)).toBe(true); // check location_admin - displayed = await common.GoToGroupPageAndGetDisplayedData( + const locationAdmin: GroupEntity = await api.getGroupEntityFromAPI( constants.GH_TEAMS["location_admin"].name, ); - expect(displayed.groupMembers).toContain(constants.GH_USERS["admin"].name); - - await page.goto("/"); - await uiHelper.openSidebar("Settings"); - await common.signOut(); + const membersLocationAdmin = parseGroupMemberFromEntity(locationAdmin); + expect( + membersLocationAdmin.includes(constants.GH_USERS["admin"].name), + ).toBe(true); }); test("Remove a user from RHDH", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("github", syncTime); - } + // remove user from RHDH -> authentication works, access is broken LOGGER.info( `Executing testcase: Remove a user from RHDH: authentication should work, but access is denied before next sync.`, ); - await common.githubLogin( - constants.GH_USERS["admin"].name, - constants.GH_USER_PASSWORD, - ); - LOGGER.info("Unregistering user1 from catalog"); - - await common.UnregisterUserEnittyFromCatalog( + LOGGER.info("Unregistering user 3 from catalog"); + await common.UnregisterUserEntityFromCatalog( constants.GH_USERS["user_1"].name, + constants.STATIC_API_TOKEN, ); - LOGGER.info("Checking alert message after login"); - await uiHelper.verifyAlertErrorMessage(/Removed entity/gm); await expect(async () => { - await common.CheckUserIsShowingInCatalog([ - constants.GH_USERS["user_1"].name, - ]); - }).not.toPass({ + expect( + await common.CheckUserIsIngestedInCatalog( + [constants.GH_USERS["user_1"].name], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ intervals: [1_000, 2_000, 5_000], - timeout: 20 * 1000, + timeout: 60 * 1000, }); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); - const loginSucceded = await common.githubLogin( constants.GH_USERS["user_1"].name, constants.GH_USER_PASSWORD, + constants.AUTH_PROVIDERS_GH_USER_2FA, ); expect(loginSucceded).toContain("Login successful"); @@ -218,6 +239,7 @@ test.describe("Standard authentication providers: Github Provider", () => { await common.githubLogin( constants.GH_USERS["user_1"].name, constants.GH_USER_PASSWORD, + constants.AUTH_PROVIDERS_GH_USER_2FA, ); await uiHelper.openSidebar("Settings"); await common.signOut(); @@ -226,31 +248,27 @@ test.describe("Standard authentication providers: Github Provider", () => { test("Remove a group from RHDH", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("github", syncTime); - } // remove group from RHDH -> user can login, but policy is broken LOGGER.info( `Executing testcase: Remove a group from RHDH: user can login, but policy is broken before next sync.`, ); - await common.githubLogin( - constants.GH_USERS["admin"].name, - constants.GH_USER_PASSWORD, - ); - await common.UnregisterGroupEnittyFromCatalog( + await common.UnregisterGroupEntityFromCatalog( constants.GH_TEAMS["team_1"].name, + constants.STATIC_API_TOKEN, ); - await uiHelper.verifyAlertErrorMessage(/Removed entity/gm); await expect(async () => { - await common.CheckGroupIsShowingInCatalog([ - constants.GH_TEAMS["team_1"].name, - ]); - }).not.toPass({ + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.GH_TEAMS["team_1"].name], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ intervals: [1_000, 2_000, 5_000], - timeout: 20 * 1000, + timeout: 60 * 1000, }); // waiting for next sync @@ -261,21 +279,22 @@ test.describe("Standard authentication providers: Github Provider", () => { `Execute testcase: Remove a group from RHDH: group is created again after the sync`, ); - await page.reload(); - - await common.CheckGroupIsShowingInCatalog([ - constants.GH_TEAMS["team_1"].name, - ]); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.GH_TEAMS["team_1"].name], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); }); test("Move a user to another group in Github", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("github", syncTime); - } + // move a user to another group -> ensure user can still login LOGGER.info( `Executing testcase: Move a user to another group in Github: user should still login before next sync.`, @@ -295,22 +314,30 @@ test.describe("Standard authentication providers: Github Provider", () => { await common.githubLogin( constants.GH_USERS["user_1"].name, constants.GH_USER_PASSWORD, + constants.AUTH_PROVIDERS_GH_USER_2FA, ); - await page.goto("/"); - await uiHelper.openSidebar("Catalog"); - // submenu with groups opens randomly in headless mode, blocking visibility of the other elements - await page.reload(); - await uiHelper.selectMuiBox("Kind", "Location"); - await uiHelper.verifyHeading("All locations"); - await uiHelper.verifyCellsInTable(["example"]); - await uiHelper.clickLink("example"); - await uiHelper.verifyHeading("example"); - await expect( - page.locator(`button[title="Schedule entity refresh"]`), - ).toHaveCount(0); - // logout - await page.goto("/"); + let apiToken; + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); + + await expect(async () => { + apiToken = await RhdhAuthHack.getInstance().getApiToken(page); + expect(apiToken).not.toBeUndefined(); + const statusBefore = await api.scheduleEntityRefreshFromAPI( + "example", + "location", + apiToken, + ); + LOGGER.info( + `Checking user can schedule location refresh. API returned ${JSON.stringify(statusBefore)}`, + ); + expect(statusBefore).toBe(403); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 90 * 1000, + }); + await uiHelper.openSidebar("Settings"); await common.signOut(); @@ -320,52 +347,72 @@ test.describe("Standard authentication providers: Github Provider", () => { LOGGER.info( `Execute testcase: Move a user to another group in Github: change should be mirrored and permission should be updated after the sync`, ); + + // location_admin should show user_2 + const group: GroupEntity = await api.getGroupEntityFromAPI( + constants.GH_TEAMS["location_admin"].name, + ); + const members = parseGroupMemberFromEntity(group); + expect(members.includes(constants.GH_USERS["user_1"].name)).toBe(true); + await common.githubLogin( constants.GH_USERS["user_1"].name, constants.GH_USER_PASSWORD, + constants.AUTH_PROVIDERS_GH_USER_2FA, ); - const displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.GH_TEAMS["location_admin"].name, - ); - expect(displayed.groupMembers).toContain(constants.GH_USERS["user_1"].name); - - // configure policy permissions different for the two groups - // after the sync, ensure the permission also reflect the user move // check RBAC permissions are updated after group update // new group should allow user to schedule location refresh and unregister the entity - await uiHelper.verifyLocationRefreshButtonIsEnabled("example"); - await page.goto("/"); + await expect(async () => { + await page.goto("/"); + await uiHelper.verifyHeading("Welcome"); + + apiToken = await RhdhAuthHack.getInstance().getApiToken(page); + const statusAfter = await api.scheduleEntityRefreshFromAPI( + "example", + "location", + apiToken, + ); + LOGGER.info( + `Checking user can schedule location refresh. API returned ${statusAfter}`, + ); + expect(statusAfter).toBe(200); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + await uiHelper.openSidebar("Settings"); await common.signOut(); + await context.clearCookies(); }); test("Remove a group from Github", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("github", syncTime); - } + // remove a group -> members still exists, member should still login LOGGER.info( - `Executing testcase: Remove a group from Microsoft EntraID: ensure group and its members still exists, member should still login before next sync.`, + `Executing testcase: Remove a group from Github: ensure group and its members still exists, member should still login before next sync.`, ); await ghHelper.deleteTeam( constants.GH_TEAMS["team_4"].name, constants.AUTH_PROVIDERS_GH_ORG_NAME, ); - // user should login - await common.githubLogin( - constants.GH_USERS["admin"].name, - constants.GH_USER_PASSWORD, - ); // team should exist in rhdh - const displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.GH_TEAMS["team_4"].name, - ); - expect(displayed.groupMembers).toContain(constants.GH_USERS["user_1"].name); + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.GH_TEAMS["team_4"].name], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); // waiting for next sync await waitForNextSync("github", syncTime); @@ -375,37 +422,44 @@ test.describe("Standard authentication providers: Github Provider", () => { `Execute testcase: Remove a group from Github: group should be removed and permissions should default to read-only after the sync.`, ); - await expect( - common.CheckGroupIsShowingInCatalog([constants.GH_USERS["user_1"].name]), - ).rejects.toThrow(); - - await page.goto("/"); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.GH_USERS["user_1"].name], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ + intervals: [5_000, 20_000], + timeout: 80 * 1000, + }); // users permission based on that group will be defaulted to read-only // expect user not to see catalog entities await common.githubLogin( constants.GH_USERS["user_1"].name, constants.GH_USER_PASSWORD, + constants.AUTH_PROVIDERS_GH_USER_2FA, ); - await page.goto("/"); - const navMyGroup = page.locator(`nav a:has-text("My Group")`); - await expect(navMyGroup).toHaveCount(0); + await expect(async () => { + await page.goto("/"); + const navMyGroup = page.locator(`nav a:has-text("My Group")`); + await expect(navMyGroup).toHaveCount(0); + }).toPass({ + intervals: [2_000, 5_000], + timeout: 30 * 1000, + }); - await page.goto("/"); await uiHelper.openSidebar("Settings"); await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + await context.clearCookies(); }); test("Rename a user and a group", async () => { test.setTimeout(600 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("github", syncTime); - } + await waitForNextSync("github", syncTime); + // rename group from RHDH -> user can login, but policy is broken LOGGER.info(`Executing testcase: Rename a user and a group.`); @@ -431,28 +485,41 @@ test.describe("Standard authentication providers: Github Provider", () => { LOGGER.info( `Execute testcase: Rename a user and a group: changes are mirrored in RHDH but permissions should be broken after the sync`, ); - await common.githubLogin( - constants.GH_USERS["admin"].name, - constants.GH_USER_PASSWORD, - ); - await common.CheckGroupIsShowingInCatalog([ - constants.GH_TEAMS["team_2"].name + "_renamed", - ]); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.MSGRAPH_GROUPS["group_6"].displayName + "_renamed"], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + expect( + await common.CheckUserIsIngestedInCatalog( + [constants.GH_TEAMS["team_2"].name + "_renamed"], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [5_000, 10_000], + timeout: 60 * 1000, + }); await common.githubLogin( constants.GH_USERS["user_1"].name, constants.GH_USER_PASSWORD, + constants.AUTH_PROVIDERS_GH_USER_2FA, ); // users permission based on that group will be defaulted to read-only // expect user not to see catalog entities - await page.goto("/"); - const navMyGroup = page.locator(`nav a:has-text("My Group")`); - await expect(navMyGroup).toHaveCount(0); + await expect(async () => { + await page.goto("/"); + const navMyGroup = page.locator(`nav a:has-text("My Group")`); + await expect(navMyGroup).toHaveCount(0); + }).toPass({ + intervals: [2_000, 5_000], + timeout: 30 * 1000, + }); // update the policy with the new group name await replaceInRBACPolicyFileConfigMap( @@ -470,7 +537,7 @@ test.describe("Standard authentication providers: Github Provider", () => { "Reloading page, permission should be updated automatically.", ); await expect(page.locator(`nav a:has-text("My Group")`)).toBeVisible({ - timeout: 2000, + timeout: 5000, }); }).toPass({ intervals: [5_000, 10_000], @@ -483,6 +550,27 @@ test.describe("Standard authentication providers: Github Provider", () => { await uiHelper.openSidebar("Settings"); await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + await context.clearCookies(); + }); + + test.afterEach(async () => { + if (test.info().status !== test.info().expectedStatus) { + const prefix = `${test.info().testId}_${test.info().retry}`; + LOGGER.info(`Dumping logs with prefix ${prefix}`); + await dumpAllPodsLogs(prefix, constants.LOGS_FOLDER); + await dumpRHDHUsersAndGroups(prefix, constants.LOGS_FOLDER); + mustSync = true; + } + }); + + test.beforeEach(async () => { + test.setTimeout(120 * 1000); + if (test.info().retry > 0 || mustSync) { + LOGGER.info( + `Waiting for sync. Retry #${test.info().retry}. Needed sync after failure: ${mustSync}.`, + ); + await waitForNextSync("github", syncTime); + mustSync = false; + } }); }); diff --git a/e2e-tests/playwright/e2e/authProviders/microsoft-provider.spec.ts b/e2e-tests/playwright/e2e/authProviders/microsoft-provider.spec.ts index d15c60687b..14189ba2f9 100644 --- a/e2e-tests/playwright/e2e/authProviders/microsoft-provider.spec.ts +++ b/e2e-tests/playwright/e2e/authProviders/microsoft-provider.spec.ts @@ -10,20 +10,28 @@ import GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupR import { waitForNextSync, replaceInRBACPolicyFileConfigMap, + parseGroupMemberFromEntity, + parseGroupChildrenFromEntity, + parseGroupParentFromEntity, + dumpAllPodsLogs, + dumpRHDHUsersAndGroups, } from "../../utils/helper"; +import { GroupEntity } from "@backstage/catalog-model"; +import { APIHelper } from "../../utils/api-helper"; +import { RhdhAuthHack } from "../../support/api/rhdh-auth-hack"; import { HelmActions } from "../../utils/helm"; let page: Page; test.describe("Standard authentication providers: Micorsoft Azure EntraID", () => { test.use({ baseURL: constants.AUTH_PROVIDERS_BASE_URL }); - let common: Common; let context: BrowserContext; let uiHelper: UIhelper; let usersCreated: Map; let groupsCreated: Map; const syncTime = 60; + let mustSync = false; test.beforeAll(async ({ browser }, testInfo) => { LOGGER.info( @@ -36,6 +44,17 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = common = new Common(page); uiHelper = new UIhelper(page); expect(process.env.BASE_URL).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_BASE_URL).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_NAMESPACE).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_RELEASE).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_CHART).not.toBeNull(); + expect(constants.CHART_VERSION).not.toBeNull(); + expect(constants.QUAY_REPO).not.toBeNull(); + expect(constants.TAG_NAME).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_VALUES_FILE).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_CHART).not.toBeNull(); + expect(constants.RBAC_POLICY_ROLES).not.toBeNull(); + LOGGER.info(`Base Url is ${process.env.BASE_URL}`); const created = await graphHelper.setupMicrosoftEntraIDEnvironment(); @@ -93,111 +112,129 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = test("Microsoft EntraID with default resolver: user_1 should login and entity is in the catalog", async () => { // resolvers from upstream are not available in rhdh // testing only default settings - LOGGER.info( "Executing testcase: Setup Microsoft EntraID with default resolver: user_1 should login and entity is in the catalog", ); test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("microsoft", syncTime); - } await common.MicrosoftAzureLogin( constants.MSGRAPH_USERS["user_1"].userPrincipalName, constants.RHSSO76_DEFAULT_PASSWORD, ); - // check no entities are in the catalog - await page.goto("/"); - await uiHelper.openSidebar("Catalog"); - await uiHelper.selectMuiBox("Kind", "User"); - await uiHelper.verifyHeading("All users"); - await uiHelper.verifyCellsInTable([ - constants.MSGRAPH_USERS["user_1"].displayName, - ]); + await expect(async () => { + expect( + await common.CheckUserIsIngestedInCatalog( + [constants.MSGRAPH_USERS["user_1"].displayName], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 90 * 1000, + }); + await uiHelper.openSidebar("Settings"); await common.signOut(); + await context.clearCookies(); }); test("Ingestion of Users and Nested Groups: verify the UserEntities and Groups are created with the correct relationships in RHDH ", async () => { test.setTimeout(300 * 1000); - await waitForNextSync("microsoft", syncTime); - - await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["admin"].userPrincipalName, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - // check entities are in the catalog const usersDisplayNames = Object.values(constants.MSGRAPH_USERS).map( (u) => u.displayName, ); - await common.CheckUserIsShowingInCatalog(usersDisplayNames); + expect( + await common.CheckUserIsIngestedInCatalog( + usersDisplayNames, + constants.STATIC_API_TOKEN, + ), + ).toBe(true); // check groups are nested correctly and display all members const groupsDisplayNames = Object.values(constants.MSGRAPH_GROUPS).map( (g) => g.displayName, ); - await common.CheckGroupIsShowingInCatalog(groupsDisplayNames); + expect( + await common.CheckGroupIsIngestedInCatalog( + groupsDisplayNames, + constants.STATIC_API_TOKEN, + ), + ).toBe(true); - let displayed; + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); // group_1 should show jenny_doe and user_1 - displayed = await common.GoToGroupPageAndGetDisplayedData( + const group1: GroupEntity = await api.getGroupEntityFromAPI( constants.MSGRAPH_GROUPS["group_1"].displayName, ); - expect(displayed.groupMembers).toContain( - constants.MSGRAPH_USERS["user_1"].displayName, - ); - expect(displayed.groupMembers).toContain( - constants.MSGRAPH_USERS["jenny_doe"].displayName, - ); - expect(displayed.childGroups).toHaveLength(0); - - // group_2 should show jenny_doe and user_2 and parent group: group_4 - displayed = await common.GoToGroupPageAndGetDisplayedData( + const members1 = parseGroupMemberFromEntity(group1); + expect( + members1.includes( + constants.MSGRAPH_USERS["user_1"].userPrincipalName.replace("@", "_"), + ), + ).toBe(true); + expect( + members1.includes( + constants.MSGRAPH_USERS["jenny_doe"].userPrincipalName.replace( + "@", + "_", + ), + ), + ).toBe(true); + expect(group1.spec.children).toHaveLength(0); + + // group_2 should show jenny_doe and user_2 + const group2: GroupEntity = await api.getGroupEntityFromAPI( constants.MSGRAPH_GROUPS["group_2"].displayName, ); - expect(displayed.groupMembers).toContain( - constants.MSGRAPH_USERS["user_2"].displayName, - ); - expect(displayed.groupMembers).toContain( - constants.MSGRAPH_USERS["jenny_doe"].displayName, - ); + expect( + parseGroupMemberFromEntity(group2).includes( + constants.MSGRAPH_USERS["user_2"].userPrincipalName.replace("@", "_"), + ), + ).toBe(true); + expect( + parseGroupMemberFromEntity(group2).includes( + constants.MSGRAPH_USERS["jenny_doe"].userPrincipalName.replace( + "@", + "_", + ), + ), + ).toBe(true); // group_3 should show user_3 and parent group: group_4 - displayed = await common.GoToGroupPageAndGetDisplayedData( + const group3: GroupEntity = await api.getGroupEntityFromAPI( constants.MSGRAPH_GROUPS["group_3"].displayName, ); - expect(displayed.groupMembers).toContain( - constants.MSGRAPH_USERS["user_3"].displayName, - ); - expect(displayed.parentGroup).toContain( + const parent3 = parseGroupParentFromEntity(group3); + expect( + parseGroupMemberFromEntity(group3).includes( + constants.MSGRAPH_USERS["user_3"].userPrincipalName.replace("@", "_"), + ), + ).toBe(true); + expect( + parent3.includes(constants.MSGRAPH_GROUPS["group_4"].displayName), + ).toBe(true); + + // group_4 should show user_4 and child group_3 + const group4: GroupEntity = await api.getGroupEntityFromAPI( constants.MSGRAPH_GROUPS["group_4"].displayName, ); - - // group_4 should show user_4 and two child groups: group_2 and group_3 - displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.MSGRAPH_GROUPS["group_4"].displayName, - ); - expect(displayed.groupMembers).toContain( - constants.MSGRAPH_USERS["user_4"].displayName, - ); - expect(displayed.childGroups).toContain( - constants.MSGRAPH_GROUPS["group_3"].displayName, - ); - - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + const children4 = parseGroupChildrenFromEntity(group4); + expect( + parseGroupMemberFromEntity(group4).includes( + constants.MSGRAPH_USERS["user_4"].userPrincipalName.replace("@", "_"), + ), + ).toBe(true); + expect( + children4.includes(constants.MSGRAPH_GROUPS["group_3"].displayName), + ).toBe(true); }); test("Remove user from Microsoft EntraID", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("microsoft", syncTime); - } - // remove user from azure -> authentication fails LOGGER.info( `Executing testcase: Remove user from Microsoft EntraID: authenticatin should fail before next sync.`, @@ -230,35 +267,41 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = // after the sync // check user_1 is deleted from user entities and group entities - await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["admin"].userPrincipalName, - constants.RHSSO76_DEFAULT_PASSWORD, - ); + await expect(async () => { + expect( + await common.CheckUserIsIngestedInCatalog( + [constants.MSGRAPH_USERS["user_1"].displayName], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ + intervals: [5_000, 10_000], + timeout: 60 * 1000, + }); - await expect( - common.CheckUserIsShowingInCatalog([ - constants.MSGRAPH_USERS["user_1"].displayName, - ]), - ).rejects.toThrow(); + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); - const displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.MSGRAPH_GROUPS["group_1"].displayName, - ); - expect(displayed.groupMembers).not.toContain( - constants.MSGRAPH_USERS["user_1"].displayName, - ); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + await expect(async () => { + const group1: GroupEntity = await api.getGroupEntityFromAPI( + constants.MSGRAPH_GROUPS["group_1"].displayName, + ); + const members1 = parseGroupMemberFromEntity(group1); + expect( + members1.includes( + constants.MSGRAPH_USERS["user_1"].userPrincipalName.replace("@", "_"), + ), + ).toBe(false); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); }); test("Move a user to another group in Microsoft EntraID", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("microsoft", syncTime); - } // move a user to another group -> user can still login - // move user_2 to group_1 + // move user_2 to location_admin LOGGER.info( `Executing testcase: Move a user to another group in Microsoft EntraID: user should still login before next sync.`, ); @@ -271,23 +314,33 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = usersCreated["user_2"], groupsCreated["group_2"], ); + await common.MicrosoftAzureLogin( constants.MSGRAPH_USERS["user_2"].userPrincipalName, constants.RHSSO76_DEFAULT_PASSWORD, ); - await page.goto("/"); - await uiHelper.openSidebar("Catalog"); - await uiHelper.selectMuiBox("Kind", "Location"); - await uiHelper.verifyHeading("All locations"); - await uiHelper.verifyCellsInTable(["example"]); - await uiHelper.clickLink("example"); - await uiHelper.verifyHeading("example"); - await expect( - page.locator(`button[title="Schedule entity refresh"]`), - ).toHaveCount(0); + let apiToken; + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); + + await expect(async () => { + apiToken = await RhdhAuthHack.getInstance().getApiToken(page); + expect(apiToken).not.toBeUndefined(); + const statusBefore = await api.scheduleEntityRefreshFromAPI( + "example", + "location", + apiToken, + ); + LOGGER.info( + `Checking user can schedule location refresh. API returned ${JSON.stringify(statusBefore)}`, + ); + expect(statusBefore).toBe(403); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 90 * 1000, + }); - await page.goto("/"); await uiHelper.openSidebar("Settings"); await common.signOut(); await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user @@ -300,23 +353,47 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = LOGGER.info( `Execute testcase: Move a user to another group in Microsoft EntraID: change should be mirrored and permission should be updated after the sync`, ); - await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["user_2"].userPrincipalName, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - const displayed = await common.GoToGroupPageAndGetDisplayedData( + // location_admin should show user_2 + const group: GroupEntity = await api.getGroupEntityFromAPI( constants.MSGRAPH_GROUPS["location_admin"].displayName, ); - expect(displayed.groupMembers).toContain( - constants.MSGRAPH_USERS["user_2"].displayName, + const members = parseGroupMemberFromEntity(group); + expect( + members.includes( + graphHelper.formatUPNToEntity( + constants.MSGRAPH_USERS["user_2"].userPrincipalName, + ), + ), + ).toBe(true); + + await common.MicrosoftAzureLogin( + constants.MSGRAPH_USERS["user_2"].userPrincipalName, + constants.RHSSO76_DEFAULT_PASSWORD, ); // check RBAC permissions are updated after group update // new group should allow user to schedule location refresh and unregister the entity - await uiHelper.verifyLocationRefreshButtonIsEnabled("example"); - await page.goto("/"); + await expect(async () => { + await page.goto("/"); + await uiHelper.verifyHeading("Welcome"); + + apiToken = await RhdhAuthHack.getInstance().getApiToken(page); + const statusAfter = await api.scheduleEntityRefreshFromAPI( + "example", + "location", + apiToken, + ); + LOGGER.info( + `Checking user can schedule location refresh. API returned ${statusAfter}`, + ); + expect(statusAfter).toBe(200); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + await uiHelper.openSidebar("Settings"); await common.signOut(); await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user @@ -324,9 +401,6 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = test("Remove a group from Microsoft EntraID", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("microsoft", syncTime); - } // remove a group -> members still exists, member should still login // remove group_3 LOGGER.info( @@ -334,23 +408,26 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = ); await graphHelper.deleteGroupByIdAsync(groupsCreated["group_3"].id); + + // group_3 should exist in rhdh + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.MSGRAPH_GROUPS["group_3"].displayName], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + // user_3 should login await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["admin"].userPrincipalName, + constants.MSGRAPH_USERS["user_3"].userPrincipalName, constants.RHSSO76_DEFAULT_PASSWORD, ); - // group_3 should exist in rhdh - const displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.MSGRAPH_GROUPS["group_3"].displayName, - ); - expect(displayed.groupMembers).toContain( - constants.MSGRAPH_USERS["user_3"].displayName, - ); - expect(displayed.parentGroup).toContain( - constants.MSGRAPH_GROUPS["group_4"].displayName, - ); - await uiHelper.openSidebar("Settings"); await common.signOut(); await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user @@ -362,19 +439,18 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = LOGGER.info( `Execute testcase: Remove a group from Microsoft EntraID: group should be removed and permissions should default to read-only after the sync.`, ); - await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["admin"].userPrincipalName, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await expect( - common.CheckGroupIsShowingInCatalog([ - constants.MSGRAPH_GROUPS["group_3"].displayName, - ]), - ).rejects.toThrow(); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.MSGRAPH_GROUPS["group_3"].displayName], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ + intervals: [5_000, 20_000], + timeout: 80 * 1000, + }); await common.MicrosoftAzureLogin( constants.MSGRAPH_USERS["user_3"].userPrincipalName, @@ -383,11 +459,15 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = // users permission based on that group will be defaulted to read-only // expect user not to see catalog entities - await page.goto("/"); - const navMyGroup = page.locator(`nav a:has-text("My Group")`); - await expect(navMyGroup).toHaveCount(0); + await expect(async () => { + await page.goto("/"); + const navMyGroup = page.locator(`nav a:has-text("My Group")`); + await expect(navMyGroup).toHaveCount(0); + }).toPass({ + intervals: [2_000, 5_000], + timeout: 30 * 1000, + }); - await page.goto("/"); await uiHelper.openSidebar("Settings"); await common.signOut(); await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user @@ -395,41 +475,36 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = test("Remove a user from RHDH", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("microsoft", syncTime); - } // remove user from RHDH -> authentication works, access is broken LOGGER.info( `Executing testcase: Remove a user from RHDH: authentication should work, but access is denied before next sync.`, ); - await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["admin"].userPrincipalName, - constants.RHSSO76_DEFAULT_PASSWORD, - ); LOGGER.info("Unregistering user 3 from catalog"); - await common.UnregisterUserEnittyFromCatalog( - constants.MSGRAPH_USERS["user_3"].displayName, + await common.UnregisterUserEntityFromCatalog( + graphHelper.formatUPNToEntity( + constants.MSGRAPH_USERS["user_3"].userPrincipalName, + ), + constants.STATIC_API_TOKEN, ); - LOGGER.info("Checking alert message after login"); - await uiHelper.verifyAlertErrorMessage(/Removed entity/gm); await expect(async () => { - await common.CheckUserIsShowingInCatalog([ - constants.RHSSO76_USERS["user_4"].firstName + - " " + - constants.RHSSO76_USERS["user_4"].lastName, - ]); - }).not.toPass({ + expect( + await common.CheckUserIsIngestedInCatalog( + [ + graphHelper.formatUPNToEntity( + constants.MSGRAPH_USERS["user_3"].displayName, + ), + ], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ intervals: [1_000, 2_000, 5_000], - timeout: 20 * 1000, + timeout: 60 * 1000, }); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user - const loginSucceded = await common.MicrosoftAzureLogin( constants.MSGRAPH_USERS["user_3"].userPrincipalName, constants.RHSSO76_DEFAULT_PASSWORD, @@ -454,6 +529,7 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = constants.MSGRAPH_USERS["user_3"].userPrincipalName, constants.RHSSO76_DEFAULT_PASSWORD, ); + await uiHelper.openSidebar("Settings"); await common.signOut(); await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user @@ -461,59 +537,61 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = test("Remove a group from RHDH", async () => { test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("microsoft", syncTime); - } - // remove group from RHDH -> user can login, but policy is broken + // remove group from RHDH -> user can login LOGGER.info( - `Executing testcase: Remove a group from RHDH: user can login, but policy is broken before next sync.`, + `Executing testcase: Remove a group from RHDH: user can login.`, ); - await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["admin"].userPrincipalName, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await common.UnregisterGroupEnittyFromCatalog( + await common.UnregisterGroupEntityFromCatalog( constants.MSGRAPH_GROUPS["group_5"].displayName, + constants.STATIC_API_TOKEN, ); - await uiHelper.verifyAlertErrorMessage(/Removed entity/gm); - await uiHelper.openSidebar("Settings"); - await common.signOut(); + + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.MSGRAPH_GROUPS["group_5"].displayName], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); await common.MicrosoftAzureLogin( constants.MSGRAPH_USERS["user_5"].userPrincipalName, constants.RHSSO76_DEFAULT_PASSWORD, ); + await uiHelper.openSidebar("Settings"); await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + await context.clearCookies(); // waiting for next sync await waitForNextSync("microsoft", syncTime); - // after sync, ensure group_5 is created again and memembers can login + // after sync, ensure group_5 is created again LOGGER.info( `Execute testcase: Remove a group from RHDH: group is created again after the sync`, ); - await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["user_5"].userPrincipalName, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await common.CheckGroupIsShowingInCatalog([ - constants.MSGRAPH_GROUPS["group_5"].displayName, - ]); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.MSGRAPH_GROUPS["group_5"].displayName], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); }); test("Rename a user and a group", async () => { test.setTimeout(600 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("microsoft", syncTime); - } - await waitForNextSync("microsoft", syncTime); // rename group from RHDH -> user can login, but policy is broken LOGGER.info(`Executing testcase: Rename a user and a group.`); @@ -533,19 +611,24 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = LOGGER.info( `Execute testcase: Rename a user and a group: changes are mirrored in RHDH but permissions should be broken after the sync`, ); - await common.MicrosoftAzureLogin( - constants.MSGRAPH_USERS["admin"].userPrincipalName, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await common.CheckUserIsShowingInCatalog([ - constants.MSGRAPH_USERS["user_6"].displayName + " Renamed", - ]); - await common.CheckGroupIsShowingInCatalog([ - constants.MSGRAPH_GROUPS["group_6"].displayName + "_renamed", - ]); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user + + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.MSGRAPH_GROUPS["group_6"].displayName + "_renamed"], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + expect( + await common.CheckUserIsIngestedInCatalog( + [usersCreated["user_6"].displayName + " Renamed"], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [5_000, 10_000], + timeout: 60 * 1000, + }); await common.MicrosoftAzureLogin( "renamed_" + constants.MSGRAPH_USERS["user_6"].userPrincipalName, @@ -554,9 +637,14 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = // users permission based on that group will be defaulted to read-only // expect user not to see catalog entities - await page.goto("/"); - const navMyGroup = page.locator(`nav a:has-text("My Group")`); - await expect(navMyGroup).toHaveCount(0); + await expect(async () => { + await page.goto("/"); + const navMyGroup = page.locator(`nav a:has-text("My Group")`); + await expect(navMyGroup).toHaveCount(0); + }).toPass({ + intervals: [2_000, 5_000], + timeout: 30 * 1000, + }); // update the policy with the new group name await replaceInRBACPolicyFileConfigMap( @@ -574,7 +662,7 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = "Reloading page, permission should be updated automatically.", ); await expect(page.locator(`nav a:has-text("My Group")`)).toBeVisible({ - timeout: 2000, + timeout: 5000, }); }).toPass({ intervals: [5_000, 10_000], @@ -589,4 +677,25 @@ test.describe("Standard authentication providers: Micorsoft Azure EntraID", () = await common.signOut(); await context.clearCookies(); // If we don't clear cookies, Microsoft Login popup will present the last logger user }); + + test.afterEach(async () => { + if (test.info().status !== test.info().expectedStatus) { + const prefix = `${test.info().testId}_${test.info().retry}`; + LOGGER.info(`Dumping logs with prefix ${prefix}`); + await dumpAllPodsLogs(prefix, constants.LOGS_FOLDER); + await dumpRHDHUsersAndGroups(prefix, constants.LOGS_FOLDER); + mustSync = true; + } + }); + + test.beforeEach(async () => { + test.setTimeout(120 * 1000); + if (test.info().retry > 0 || mustSync) { + LOGGER.info( + `Waiting for sync. Retry #${test.info().retry}. Needed sync after failure: ${mustSync}.`, + ); + await waitForNextSync("microsoft", syncTime); + mustSync = false; + } + }); }); diff --git a/e2e-tests/playwright/e2e/authProviders/rhsso-76-provider.spec.ts b/e2e-tests/playwright/e2e/authProviders/rhsso-76-provider.spec.ts index e992b3d094..bab8117d72 100644 --- a/e2e-tests/playwright/e2e/authProviders/rhsso-76-provider.spec.ts +++ b/e2e-tests/playwright/e2e/authProviders/rhsso-76-provider.spec.ts @@ -8,662 +8,763 @@ import GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupR import { waitForNextSync, replaceInRBACPolicyFileConfigMap, + dumpAllPodsLogs, + dumpRHDHUsersAndGroups, } from "../../utils/helper"; -import * as rhssoHelper from "../../utils/authenticationProviders/rh-sso-helper"; import { HelmActions } from "../../utils/helm"; +import { GroupEntity } from "@backstage/catalog-model"; +import { APIHelper } from "../../utils/api-helper"; +import { RHSSOHelper } from "../../utils/authenticationProviders/rh-sso-helper"; +import { RhdhAuthHack } from "../../support/api/rhdh-auth-hack"; let page: Page; -test.describe("Standard authentication providers: OIDC with RHSSO 7.6", () => { - test.use({ baseURL: constants.AUTH_PROVIDERS_BASE_URL }); - - let common: Common; - let uiHelper: UIhelper; - let usersCreated: Map; - let groupsCreated: Map; - const syncTime = 60; - - test.beforeAll(async ({ browser }, testInfo) => { - LOGGER.info( - `Staring scenario: Standard authentication providers: OIDC with RHSSO 7.6: attemp #${testInfo.retry}`, - ); - expect(process.env.BASE_URL).not.toBeNull(); - LOGGER.info(`Base Url is ${process.env.BASE_URL}`); - - page = (await setupBrowser(browser, testInfo)).page; - common = new Common(page); - uiHelper = new UIhelper(page); - - await rhssoHelper.initializeRHSSOClient(rhssoHelper.CONNECTION_CONFIG); - const created = await rhssoHelper.setupRHSSOEnvironment(); - usersCreated = created.usersCreated; - groupsCreated = created.groupsCreated; - }); +for (const version of ["RHBK", "RHSSO"]) { + test.describe(`Standard authentication providers: OIDC with ${version}`, () => { + test.use({ baseURL: constants.AUTH_PROVIDERS_BASE_URL }); - test("Default resolver for RHSSO should be emailLocalPartMatchingUserEntityName: user_1 should authenticate, user_2 should not", async () => { - test.setTimeout(600 * 1000); - - LOGGER.info( - `Executing testcase: Default resolver for RHSSO should be emailLocalPartMatchingUserEntityName: user_1 should authenticate, user_2 should not`, - ); - // setup RHSSO provider with user ingestion - await HelmActions.upgradeHelmChartWithWait( - constants.AUTH_PROVIDERS_RELEASE, - constants.AUTH_PROVIDERS_CHART, - constants.AUTH_PROVIDERS_NAMESPACE, - constants.AUTH_PROVIDERS_VALUES_FILE, - constants.CHART_VERSION, - constants.QUAY_REPO, - constants.TAG_NAME, - [ - "--set global.dynamic.plugins[0].disabled=false", - "--set upstream.postgresql.primary.persistence.enabled=false", - "--set upstream.backstage.appConfig.catalog.providers.githubOrg=null", - "--set upstream.backstage.appConfig.catalog.providers.microsoftOrg=null", - "--set global.dynamic.plugins[3].disabled=false", - "--set upstream.backstage.appConfig.permission.enabled=true", - "--set upstream.backstage.appConfig.auth.providers.oidc.production.callbackUrl=${RHSSO76_CALLBACK_URL}", - ], - ); - - await waitForNextSync("rhsso", syncTime); - - await common.keycloakLogin( - constants.RHSSO76_USERS["user_1"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await uiHelper.openSidebar("Settings"); - await uiHelper.verifyHeading( - await rhssoHelper.getRHSSOUserDisplayName( - constants.RHSSO76_USERS["user_1"], - ), - ); - await common.signOut(); - - await common.keycloakLogin( - constants.RHSSO76_USERS["user_2"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await uiHelper.verifyAlertErrorMessage( - "Login failed; caused by NotFoundError: User not found", - ); - - await rhssoHelper.clearUserSessions( - constants.RHSSO76_USERS["user_2"].username, - constants.AUTH_PROVIDERS_REALM_NAME, - ); - }); + let common: Common; + let uiHelper: UIhelper; + let usersCreated: Map; + let groupsCreated: Map; + const syntTime = 60; + let mustSync = false; + let rhssoHelper: RHSSOHelper; - test("Testing resolver emailMatchingUserEntityProfileEmail: user_1 should authenticate, jdoe should not", async () => { - test.setTimeout(600 * 1000); - LOGGER.info( - "Executing testcase: Testing resolver emailMatchingUserEntityProfileEmail: user_1 should authenticate, jdoe should not", - ); - // updating the resolver - // disable keycloak plugin to disable ingestion - // edit jdoe user in keycloak to have a different email than the synced one: it will not be synced - - await HelmActions.upgradeHelmChartWithWait( - constants.AUTH_PROVIDERS_RELEASE, - constants.AUTH_PROVIDERS_CHART, - constants.AUTH_PROVIDERS_NAMESPACE, - constants.AUTH_PROVIDERS_VALUES_FILE, - constants.CHART_VERSION, - constants.QUAY_REPO, - constants.TAG_NAME, - [ - "--set global.dynamic.plugins[0].disabled=false", - "--set global.dynamic.plugins[1].disabled=true", - "--set global.dynamic.plugins[2].disabled=true", - "--set upstream.postgresql.primary.persistence.enabled=false", - "--set upstream.backstage.appConfig.auth.providers.oidc.production.signIn.resolvers[0].resolver=emailMatchingUserEntityProfileEmail", - "--set global.dynamic.plugins[3].disabled=false", - "--set upstream.backstage.appConfig.permission.enabled=true", - "--set upstream.backstage.appConfig.auth.providers.oidc.production.callbackUrl=${RHSSO76_CALLBACK_URL}", - ], - ); - - await waitForNextSync("rhsso", syncTime); - - // emailMatchingUserEntityProfileEmail should only allow authentication of keycloak users that match the email attribute with the entity one. - // update jdoe email -> login should fail with error Login failed; caused by Error: Failed to sign-in, unable to resolve user identity - await rhssoHelper.updateUserEmail( - constants.RHSSO76_USERS["jenny_doe"].username, - constants.JDOE_NEW_EMAIL, - ); - - // login with testuser1 -> login should succeed - await common.keycloakLogin( - constants.RHSSO76_USERS["user_1"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await uiHelper.openSidebar("Settings"); - await uiHelper.verifyHeading( - await rhssoHelper.getRHSSOUserDisplayName( - constants.RHSSO76_USERS["user_1"], - ), - ); - await common.signOut(); - - // login with jenny doe -> should fail - await common.keycloakLogin( - constants.RHSSO76_USERS["jenny_doe"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await uiHelper.verifyAlertErrorMessage( - "Login failed; caused by Error: Failed to sign-in, unable to resolve user identity", - ); - await rhssoHelper.clearUserSessions( - constants.RHSSO76_USERS["jenny_doe"].username, - constants.AUTH_PROVIDERS_REALM_NAME, - ); - }); + let helmParams = []; - test("Testing resolver preferredUsernameMatchingUserEntityName: user_1 and jenny_doe should both authenticate", async () => { - test.setTimeout(600 * 1000); - LOGGER.info( - "Executing testcase: Testing resolver preferredUsernameMatchingUserEntityName: user_1 and jenny_doe should both authenticate", - ); - // updating the resolver - // disable keycloak plugin to disable ingestion - - await HelmActions.upgradeHelmChartWithWait( - constants.AUTH_PROVIDERS_RELEASE, - constants.AUTH_PROVIDERS_CHART, - constants.AUTH_PROVIDERS_NAMESPACE, - constants.AUTH_PROVIDERS_VALUES_FILE, - constants.CHART_VERSION, - constants.QUAY_REPO, - constants.TAG_NAME, - [ - "--set global.dynamic.plugins[0].disabled=false", - "--set global.dynamic.plugins[1].disabled=true", - "--set global.dynamic.plugins[2].disabled=true", - "--set upstream.postgresql.primary.persistence.enabled=false", - "--set upstream.backstage.appConfig.auth.providers.oidc.production.signIn.resolvers[0].resolver=preferredUsernameMatchingUserEntityName", - "--set global.dynamic.plugins[3].disabled=false", - "--set upstream.backstage.appConfig.permission.enabled=true", - "--set upstream.backstage.appConfig.auth.providers.oidc.production.callbackUrl=${RHSSO76_CALLBACK_URL}", - ], - ); - - // preferredUsernameMatchingUserEntityName should allow authentication of any keycloak. - - await waitForNextSync("rhsso", syncTime); - - // login with testuser1 -> login should succeed - await common.keycloakLogin( - constants.RHSSO76_USERS["user_1"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await uiHelper.openSidebar("Settings"); - await uiHelper.verifyHeading( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_1"]), - ); - await common.signOut(); - - // login with jenny doe -> should succeed - await common.keycloakLogin( - constants.RHSSO76_USERS["jenny_doe"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await uiHelper.openSidebar("Settings"); - await uiHelper.verifyHeading( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["jenny_doe"]), - ); - await common.signOut(); - - // login with user_2 -> should succeed - await common.keycloakLogin( - constants.RHSSO76_USERS["user_2"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await uiHelper.openSidebar("Settings"); - await uiHelper.verifyHeading( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_2"]), - ); - await common.signOut(); - }); + test.beforeAll(async ({ browser }, testInfo) => { + test.setTimeout(120 * 1000); + LOGGER.info( + `Staring scenario: Standard authentication providers: OIDC with ${version}: attemp #${testInfo.retry}`, + ); + expect(constants.RHSSO76_ADMIN_USERNAME).not.toBeNull(); + expect(constants.RHSSO76_ADMIN_PASSWORD).not.toBeNull(); + expect(constants.RHSSO76_DEFAULT_PASSWORD).not.toBeNull(); + expect(constants.RHSSO76_URL).not.toBeNull(); + expect(constants.RHSSO76_CLIENT_SECRET).not.toBeNull(); + expect(constants.RHSSO76_CLIENTID).not.toBeNull(); + + expect(constants.RHBK_ADMIN_USERNAME).not.toBeNull(); + expect(constants.RHBK_ADMIN_PASSWORD).not.toBeNull(); + expect(constants.RHBK_DEFAULT_PASSWORD).not.toBeNull(); + expect(constants.RHBK_URL).not.toBeNull(); + expect(constants.RHBK_CLIENT_SECRET).not.toBeNull(); + expect(constants.RHBK_CLIENTID).not.toBeNull(); + + expect(constants.RHSSO76_GROUPS).not.toBeNull(); + expect(constants.RHSSO76_NESTED_GROUP).not.toBeNull(); + expect(constants.RHSSO76_USERS).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_REALM_NAME).not.toBeNull(); + + expect(constants.AUTH_PROVIDERS_BASE_URL).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_NAMESPACE).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_RELEASE).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_CHART).not.toBeNull(); + expect(constants.CHART_VERSION).not.toBeNull(); + expect(constants.QUAY_REPO).not.toBeNull(); + expect(constants.TAG_NAME).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_VALUES_FILE).not.toBeNull(); + expect(constants.AUTH_PROVIDERS_CHART).not.toBeNull(); + expect(constants.RBAC_POLICY_ROLES).not.toBeNull(); + expect(constants.STATIC_API_TOKEN).not.toBeNull(); + + LOGGER.info(`Base Url is ${process.env.BASE_URL}`); + + page = (await setupBrowser(browser, testInfo)).page; + common = new Common(page); + uiHelper = new UIhelper(page); + + if (version == "RHSSO") { + helmParams = []; + } else if (version == "RHBK") { + helmParams = [ + "--values ../.ibm/pipelines/value_files/values_showcase-auth-provider-diff-rhbk.yaml", + ]; + } + + rhssoHelper = new RHSSOHelper(version); + await rhssoHelper.initializeRHSSOClient(); + const created = await rhssoHelper.setupRHSSOEnvironment(); + usersCreated = created.usersCreated; + groupsCreated = created.groupsCreated; + }); - test("Ingestion of Users and Nested Groups: verify the UserEntities and Groups are created with the correct relationships in RHDH", async () => { - if (test.info().retry > 0) { - await waitForNextSync("rhsso", syncTime); - } - await common.keycloakLogin( - constants.RHSSO76_USERS["user_1"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - // check entities are in the catalog - const usersDisplayNames = Object.values(constants.RHSSO76_USERS).map((u) => - rhssoHelper.getRHSSOUserDisplayName(u), - ); - - await common.CheckUserIsShowingInCatalog(usersDisplayNames); - - // check groups are nested correctly and display all members - const groupsDisplayNames = Object.values(constants.RHSSO76_GROUPS).map( - (g) => g.name, - ); - groupsDisplayNames.push(constants.RHSSO76_NESTED_GROUP.name); - await common.CheckGroupIsShowingInCatalog(groupsDisplayNames); - - let displayed; - - // group_1 should show user_1 - displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.RHSSO76_GROUPS["group_1"].name, - ); - - expect(displayed.groupMembers).toContain( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_1"]), - ); - - // group_2 should show user_2 and parent group_nested - displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.RHSSO76_GROUPS["group_2"].name, - ); - - expect(displayed.groupMembers).toContain( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_2"]), - ); - expect(displayed.childGroups).toContain( - constants.RHSSO76_NESTED_GROUP.name, - ); - - // group_nested should show user_3 - displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.RHSSO76_NESTED_GROUP.name, - ); - - expect(displayed.groupMembers).toContain( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_3"]), - ); - expect(displayed.parentGroup).toContain( - constants.RHSSO76_GROUPS["group_2"].name, - ); - - // logout - await uiHelper.openSidebar("Settings"); - await common.signOut(); - }); + test(`${version} - default resolver should be emailLocalPartMatchingUserEntityName: user_1 should authenticate, user_2 should not`, async () => { + test.setTimeout(600 * 1000); + + LOGGER.info(`Executing testcase: ${test.info().title}`); + // setup RHSSO provider with user ingestion + await HelmActions.upgradeHelmChartWithWait( + constants.AUTH_PROVIDERS_RELEASE, + constants.AUTH_PROVIDERS_CHART, + constants.AUTH_PROVIDERS_NAMESPACE, + constants.AUTH_PROVIDERS_VALUES_FILE, + constants.CHART_VERSION, + constants.QUAY_REPO, + constants.TAG_NAME, + [ + "--set global.dynamic.plugins[0].disabled=false", + "--set upstream.postgresql.primary.persistence.enabled=false", + "--set upstream.backstage.appConfig.catalog.providers.githubOrg=null", + "--set upstream.backstage.appConfig.catalog.providers.microsoftOrg=null", + "--set global.dynamic.plugins[3].disabled=false", + "--set upstream.backstage.appConfig.permission.enabled=true", + ...helmParams, + ], + ); + + await waitForNextSync("rhsso", syntTime); + + await common.keycloakLogin( + constants.RHSSO76_USERS["user_1"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + await uiHelper.openSidebar("Settings"); + await uiHelper.verifyHeading( + await rhssoHelper.getRHSSOUserDisplayName( + constants.RHSSO76_USERS["user_1"], + ), + ); + await common.signOut(); + + await common.keycloakLogin( + constants.RHSSO76_USERS["user_2"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + await uiHelper.verifyAlertErrorMessage( + "Login failed; caused by NotFoundError: User not found", + ); + + await rhssoHelper.clearUserSessions( + constants.RHSSO76_USERS["user_2"].username, + constants.AUTH_PROVIDERS_REALM_NAME, + ); + }); + + test(`${version} - testing resolver emailMatchingUserEntityProfileEmail: user_1 should authenticate, jdoe should not`, async () => { + test.setTimeout(600 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); + + // updating the resolver + // disable keycloak plugin to disable ingestion + // edit jdoe user in keycloak to have a different email than the synced one: it will not be synced + + await HelmActions.upgradeHelmChartWithWait( + constants.AUTH_PROVIDERS_RELEASE, + constants.AUTH_PROVIDERS_CHART, + constants.AUTH_PROVIDERS_NAMESPACE, + constants.AUTH_PROVIDERS_VALUES_FILE, + constants.CHART_VERSION, + constants.QUAY_REPO, + constants.TAG_NAME, + [ + "--set global.dynamic.plugins[0].disabled=false", + "--set global.dynamic.plugins[1].disabled=true", + "--set global.dynamic.plugins[2].disabled=true", + "--set upstream.postgresql.primary.persistence.enabled=false", + "--set upstream.backstage.appConfig.auth.providers.oidc.production.signIn.resolvers[0].resolver=emailMatchingUserEntityProfileEmail", + "--set global.dynamic.plugins[3].disabled=false", + "--set upstream.backstage.appConfig.permission.enabled=true", + ...helmParams, + ], + ); + + await waitForNextSync("rhsso", syntTime); + + // emailMatchingUserEntityProfileEmail should only allow authentication of keycloak users that match the email attribute with the entity one. + // update jdoe email -> login should fail with error Login failed; caused by Error: Failed to sign-in, unable to resolve user identity + await rhssoHelper.updateUserEmail( + constants.RHSSO76_USERS["jenny_doe"].username, + constants.JDOE_NEW_EMAIL, + ); + + // login with testuser1 -> login should succeed + await common.keycloakLogin( + constants.RHSSO76_USERS["user_1"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + await uiHelper.openSidebar("Settings"); + await uiHelper.verifyHeading( + await rhssoHelper.getRHSSOUserDisplayName( + constants.RHSSO76_USERS["user_1"], + ), + ); + await common.signOut(); + + // login with jenny doe -> should fail + await common.keycloakLogin( + constants.RHSSO76_USERS["jenny_doe"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + await uiHelper.verifyAlertErrorMessage( + /Login failed; caused by Error: Failed to sign-in, unable to resolve user identity/gm, + ); + await rhssoHelper.clearUserSessions( + constants.RHSSO76_USERS["jenny_doe"].username, + constants.AUTH_PROVIDERS_REALM_NAME, + ); + }); + + test(`${version} - testing resolver preferredUsernameMatchingUserEntityName: user_1 and jenny_doe should both authenticate`, async () => { + test.setTimeout(600 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); + // updating the resolver + // disable keycloak plugin to disable ingestion + + await HelmActions.upgradeHelmChartWithWait( + constants.AUTH_PROVIDERS_RELEASE, + constants.AUTH_PROVIDERS_CHART, + constants.AUTH_PROVIDERS_NAMESPACE, + constants.AUTH_PROVIDERS_VALUES_FILE, + constants.CHART_VERSION, + constants.QUAY_REPO, + constants.TAG_NAME, + [ + "--set global.dynamic.plugins[0].disabled=false", + "--set global.dynamic.plugins[1].disabled=true", + "--set global.dynamic.plugins[2].disabled=true", + "--set upstream.postgresql.primary.persistence.enabled=false", + "--set upstream.backstage.appConfig.auth.providers.oidc.production.signIn.resolvers[0].resolver=preferredUsernameMatchingUserEntityName", + "--set global.dynamic.plugins[3].disabled=false", + "--set upstream.backstage.appConfig.permission.enabled=true", + ...helmParams, + ], + ); - test("Remove user from RHSSO", async () => { - // remove user from azure -> ensure authentication fails - test.setTimeout(300 * 1000); - LOGGER.info(`Executing testcase: Remove user from RHSSO`); - if (test.info().retry > 0) { - await waitForNextSync("rhsso", syncTime); - } - await rhssoHelper.deleteUser(usersCreated["user_1"].id); - await page.waitForTimeout(2000); // give rhsso a few seconds - await common.keycloakLogin( - constants.RHSSO76_USERS["user_1"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - await uiHelper.verifyAlertErrorMessage(/Login failed/gm); - - await waitForNextSync("rhsso", syncTime); - - await common.keycloakLogin( - constants.RHSSO76_USERS["user_2"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - await expect( - common.CheckUserIsShowingInCatalog([ + // preferredUsernameMatchingUserEntityName should allow authentication of any keycloak. + + await waitForNextSync("rhsso", syntTime); + + // login with testuser1 -> login should succeed + await common.keycloakLogin( + constants.RHSSO76_USERS["user_1"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + await uiHelper.openSidebar("Settings"); + await uiHelper.verifyHeading( rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_1"]), - ]), - ).rejects.toThrow(); - - const displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.RHSSO76_GROUPS["group_1"].name, - ); - expect(displayed.groupMembers).not.toContain( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_1"]), - ); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - }); + ); + await common.signOut(); - test("Move a user to another group in RHSSO", async () => { - test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("rhsso", syncTime); - } - // move a user to another group -> ensure user can still login - // move user_3 to group_3 - LOGGER.info( - `Executing testcase: Move a user to another group in Microsoft EntraID: user should still login before next sync.`, - ); - - await rhssoHelper.removeUserFromGroup( - usersCreated["user_3"].id, - groupsCreated["group_4"].id, - ); - await rhssoHelper.addUserToGroup( - usersCreated["user_3"].id, - groupsCreated["location_admin"].id, - ); - await common.keycloakLogin( - constants.RHSSO76_USERS["user_3"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await page.goto("/"); - await uiHelper.openSidebar("Catalog"); - // submenu with groups opens randomly in headless mode, blocking visibility of the other elements - await page.mouse.move( - page.viewportSize().width / 2, - page.viewportSize().height / 2, - ); - await uiHelper.selectMuiBox("Kind", "Location"); - await uiHelper.verifyHeading("All locations"); - await uiHelper.verifyCellsInTable(["example"]); - await uiHelper.clickLink("example"); - await uiHelper.verifyHeading("example"); - await expect( - page.locator(`button[title="Schedule entity refresh"]`), - ).toHaveCount(0); - // logout - await page.goto("/"); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - - await waitForNextSync("rhsso", syncTime); - - // ensure the change is mirrored in the catalog - LOGGER.info( - `Execute testcase: Move a user to another group in RHSSO: change should be mirrored and permission should be updated after the sync`, - ); - await common.keycloakLogin( - constants.RHSSO76_USERS["user_3"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - const displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.RHSSO76_GROUPS["location_admin"].name, - ); - expect(displayed.groupMembers).toContain( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_3"]), - ); - - // configure policy permissions different for the two groups - // after the sync, ensure the permission also reflect the user move - // check RBAC permissions are updated after group update - // new group should allow user to schedule location refresh and unregister the entity - await page.goto("/"); - await uiHelper.openSidebar("Catalog"); - // submenu with groups opens randomly in headless mode, blocking visibility of the other elements - await page.mouse.move( - page.viewportSize().width / 2, - page.viewportSize().height / 2, - ); - - await await uiHelper.verifyLocationRefreshButtonIsEnabled("example"); - - await page.goto("/"); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - }); + // login with jenny doe -> should succeed + await common.keycloakLogin( + constants.RHSSO76_USERS["jenny_doe"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + await uiHelper.openSidebar("Settings"); + await uiHelper.verifyHeading( + rhssoHelper.getRHSSOUserDisplayName( + constants.RHSSO76_USERS["jenny_doe"], + ), + ); + await common.signOut(); + + // login with user_2 -> should succeed + await common.keycloakLogin( + constants.RHSSO76_USERS["user_2"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + await uiHelper.openSidebar("Settings"); + await uiHelper.verifyHeading( + rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_2"]), + ); + await common.signOut(); + }); + + test(`${version} - ingestion of Users and Nested Groups: verify the UserEntities and Groups are created with the correct relationships in RHDH`, async () => { + test.setTimeout(300 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); + + // check users are in the catalog + const usersDisplayNames = Object.values(constants.RHSSO76_USERS).map( + (u) => rhssoHelper.getRHSSOUserDisplayName(u), + ); + + expect( + await common.CheckUserIsIngestedInCatalog( + usersDisplayNames, + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + + // check groups are in the catalog + const groupsDisplayNames = Object.values(constants.RHSSO76_GROUPS).map( + (g) => g.name, + ); + groupsDisplayNames.push(constants.RHSSO76_NESTED_GROUP.name); + expect( + await common.CheckGroupIsIngestedInCatalog( + groupsDisplayNames, + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); + + // group_1 should show user_1 + const group1: GroupEntity = await api.getGroupEntityFromAPI( + constants.RHSSO76_GROUPS["group_1"].name, + ); + expect( + group1.spec.members.includes( + constants.RHSSO76_USERS["user_1"].username, + ), + ).toBe(true); + + // group_2 should show user_2 and parent group_nested + const group2: GroupEntity = await api.getGroupEntityFromAPI( + constants.RHSSO76_GROUPS["group_2"].name, + ); + + expect( + group2.spec.members.includes( + constants.RHSSO76_USERS["user_2"].username, + ), + ).toBe(true); + expect( + group2.spec.children.includes(constants.RHSSO76_NESTED_GROUP.name), + ).toBe(true); + + // group_nested should show user_3 + const group3: GroupEntity = await api.getGroupEntityFromAPI( + constants.RHSSO76_NESTED_GROUP.name, + ); - test("Remove a group from RHSSO", async () => { - test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("rhsso", syncTime); - } - // remove a group -> ensure group and its members still exists, member should still login - // remove group_3 - LOGGER.info( - `Executing testcase: Remove a group from RHSSO: ensure group and its members still exists, member should still login before next sync.`, - ); - - await rhssoHelper.deleteGroup(groupsCreated["group_4"].id); - // user_4 should login - await common.keycloakLogin( - constants.RHSSO76_USERS["user_4"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - // group_4 should exist in rhdh - const displayed = await common.GoToGroupPageAndGetDisplayedData( - constants.RHSSO76_GROUPS["group_4"].name, - ); - expect(displayed.groupMembers).toContain( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_4"]), - ); - - await uiHelper.openSidebar("Settings"); - await common.signOut(); - - // waiting for next sync - await waitForNextSync("rhsso", syncTime); - - // after the sync ensure the group entity is removed - LOGGER.info( - `Execute testcase: Remove a group from RHSSO: group should be removed and permissions should default to read-only after the sync.`, - ); - await common.keycloakLogin( - constants.RHSSO76_USERS["admin"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - await expect( - common.CheckGroupIsShowingInCatalog([ + expect( + group3.spec.members.includes( + constants.RHSSO76_USERS["user_3"].username, + ), + ).toBe(true); + + // group_4 should show user_3 + const group4: GroupEntity = await api.getGroupEntityFromAPI( constants.RHSSO76_GROUPS["group_4"].name, - ]), - ).rejects.toThrow(); - - await uiHelper.openSidebar("Settings"); - await common.signOut(); - - await common.keycloakLogin( - constants.RHSSO76_USERS["user_4"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - // users permission based on that group will be defaulted to read-only - // expect user not to see catalog entities - await page.goto("/"); - const navMyGroup = page.locator(`nav a:has-text("My Group")`); - await expect(navMyGroup).toHaveCount(0); - - await page.goto("/"); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - }); + ); - test("Remove a user from RHDH", async () => { - test.setTimeout(300 * 1000); - - // remove user from RHDH -> authentication works, access is broken - LOGGER.info( - `Executing testcase: Remove a user from RHDH: authentication should work, but access is denied before next sync.`, - ); - if (test.info().retry > 0) { - await waitForNextSync("rhsso", syncTime); - } - await common.keycloakLogin( - constants.RHSSO76_USERS["admin"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await common.UnregisterUserEnittyFromCatalog( - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_4"]), - ); - await uiHelper.verifyAlertErrorMessage(/Removed entity/gm); - - await expect(async () => { - await common.CheckUserIsShowingInCatalog([ - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_4"]), - ]); - }).not.toPass({ - intervals: [1_000, 2_000, 5_000], - timeout: 20 * 1000, + expect( + group4.spec.members.includes( + constants.RHSSO76_USERS["user_3"].username, + ), + ).toBe(true); + + expect( + group4.spec.members.includes( + constants.RHSSO76_USERS["user_4"].username, + ), + ).toBe(true); }); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - - const loginSucceded = await common.keycloakLogin( - constants.RHSSO76_USERS["user_4"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - expect(loginSucceded).toContain("Login successful"); - - await uiHelper.verifyAlertErrorMessage(/unable to resolve user identity/gm); - - // clear user sessions - await rhssoHelper.clearUserSessions( - constants.RHSSO76_USERS["user_4"].username, - constants.AUTH_PROVIDERS_REALM_NAME, - ); - - await waitForNextSync("rhsso", syncTime); - - LOGGER.info( - `Execute testcase: Remove a user from RHDH: user is re-created and can login after the sync`, - ); - await common.keycloakLogin( - constants.RHSSO76_USERS["user_4"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await common.CheckUserIsShowingInCatalog([ - rhssoHelper.getRHSSOUserDisplayName(constants.RHSSO76_USERS["user_4"]), - ]); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - }); + test(` ${version} - remove user from ${version}`, async () => { + test.setTimeout(300 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); - test("Remove a group from RHDH", async () => { - test.setTimeout(300 * 1000); - - // remove group from RHDH -> user can login, but policy is broken - LOGGER.info( - `Executing testcase: Remove a group from RHDH: user can login, but policy is broken before next sync.`, - ); - if (test.info().retry >= 0) { - await waitForNextSync("rhsso", syncTime); - } - await common.keycloakLogin( - constants.RHSSO76_USERS["admin"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - await common.UnregisterGroupEnittyFromCatalog( - constants.RHSSO76_GROUPS["group_3"].name, - ); - - await uiHelper.verifyAlertErrorMessage(/Removed entity/gm); - - await expect( - common.CheckGroupIsShowingInCatalog([ - constants.RHSSO76_GROUPS["group_3"].name, - ]), - ).rejects.toThrow(/Expected at least one cell/); - - await uiHelper.openSidebar("Settings"); - await common.signOut(); - - await waitForNextSync("rhsso", syncTime); - - // after sync, ensure group is created again and memembers can login - LOGGER.info( - `Execute testcase: Remove a group from RHDH: group is created again after the sync`, - ); - await common.keycloakLogin( - constants.RHSSO76_USERS["admin"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await common.CheckGroupIsShowingInCatalog([ - constants.RHSSO76_GROUPS["group_3"].name, - ]); - await uiHelper.openSidebar("Settings"); - await common.signOut(); - }); + await rhssoHelper.deleteUser(usersCreated["user_1"].id); + await page.waitForTimeout(2000); // give rhsso a few seconds + await common.keycloakLogin( + constants.RHSSO76_USERS["user_1"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + + await uiHelper.verifyAlertErrorMessage(/Login failed/gm); + + await waitForNextSync("rhsso", syntTime); + + await expect(async () => { + expect( + await common.CheckUserIsIngestedInCatalog( + [ + rhssoHelper.getRHSSOUserDisplayName( + constants.RHSSO76_USERS["user_1"], + ), + ], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); + + await expect(async () => { + const group1: GroupEntity = await api.getGroupEntityFromAPI( + constants.RHSSO76_GROUPS["group_1"].name, + ); + expect( + group1.spec.members.includes( + constants.RHSSO76_USERS["user_1"].username, + ), + ).toBe(false); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + }); - test("Rename a user and a group", async () => { - test.setTimeout(300 * 1000); - if (test.info().retry > 0) { - await waitForNextSync("rhsso", syncTime); - } + test(`${version} - move a user to another group in ${version}`, async () => { + test.setTimeout(300 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); - // rename group -> user can login, but policy is broken - LOGGER.info(`Executing testcase: Rename a user and a group.`); + // move a user to another group -> ensure user can still login + // move user_3 to group_3 - await rhssoHelper.updateUser(usersCreated["user_2"].id, { - lastName: constants.RHSSO76_USERS["user_2"].lastName + " Renamed", - emailVerified: true, - email: constants.RHSSO76_USERS["user_2"].username + "@rhdh.com", + await rhssoHelper.removeUserFromGroup( + usersCreated["user_3"].id, + groupsCreated["group_4"].id, + ); + await rhssoHelper.addUserToGroup( + usersCreated["user_3"].id, + groupsCreated["location_admin"].id, + ); + + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); + let apiToken: string; + + await expect(async () => { + await common.keycloakLogin( + constants.RHSSO76_USERS["user_3"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + + apiToken = await RhdhAuthHack.getInstance().getApiToken(page); + expect(apiToken).not.toBeUndefined(); + + const statusBefore = await api.scheduleEntityRefreshFromAPI( + "example", + "location", + apiToken, + ); + LOGGER.info( + `Checking user can schedule location refresh. API returned ${JSON.stringify(statusBefore)}`, + ); + expect(statusBefore).toBe(403); + + // logout + await uiHelper.openSidebar("Settings"); + await common.signOut(); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 90 * 1000, + }); + + await waitForNextSync("rhsso", syntTime); + + // ensure the change is mirrored in the catalog + // location_admin should show user_3 + await expect(async () => { + const group3: GroupEntity = await api.getGroupEntityFromAPI( + constants.RHSSO76_GROUPS["location_admin"].name, + ); + expect( + group3.spec.members?.includes( + constants.RHSSO76_USERS["user_3"].username, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + + // configure policy permissions different for the two groups + // after the sync, ensure the permission also reflect the user move + await common.keycloakLogin( + constants.RHSSO76_USERS["user_3"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + + // check RBAC permissions are updated after group update + // new group should allow user to schedule location refresh and unregister the entity + await expect(async () => { + await page.goto("/"); + await uiHelper.verifyHeading("Welcome"); + + apiToken = await RhdhAuthHack.getInstance().getApiToken(page); + expect(apiToken).not.toBeNull(); + const statusAfter = await api.scheduleEntityRefreshFromAPI( + "example", + "location", + apiToken, + ); + LOGGER.info( + `Checking user can schedule location refresh. API returned ${statusAfter}`, + ); + expect(statusAfter).toBe(200); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + + await uiHelper.openSidebar("Settings"); + await common.signOut(); }); - await rhssoHelper.updateGruop(groupsCreated["group_2"].id, { - name: constants.RHSSO76_GROUPS["group_2"].name + "_renamed", + test(`${version} - remove a group from ${version}`, async () => { + test.setTimeout(300 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); + + // remove a group -> ensure group and its members still exists, member should still login + // remove group_3 + + await rhssoHelper.deleteGroup(groupsCreated["group_4"].id); + + // user_4 should login + await common.keycloakLogin( + constants.RHSSO76_USERS["user_4"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + + await uiHelper.openSidebar("Settings"); + await common.signOut(); + + // group_4 should exist in rhdh + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); + + await expect(async () => { + const group4: GroupEntity = await api.getGroupEntityFromAPI( + constants.RHSSO76_GROUPS["group_4"].name, + ); + expect( + group4.spec.members.includes( + constants.RHSSO76_USERS["user_4"].username, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + + // waiting for next sync + await waitForNextSync("rhsso", syntTime); + + // after the sync ensure the group entity is removed + // group_4 should not be in the catalog anymore + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.RHSSO76_GROUPS["group_4"].name], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + + // users permission based on that group will be defaulted to read-only + // expect user not to see catalog entities + await common.keycloakLogin( + constants.RHSSO76_USERS["user_4"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + + await expect(async () => { + await page.goto("/"); + const navMyGroup = page.locator(`nav a:has-text("My Group")`); + await expect(navMyGroup).toHaveCount(0); + }).toPass({ + intervals: [2_000, 5_000], + timeout: 30 * 1000, + }); + + await uiHelper.openSidebar("Settings"); + await common.signOut(); }); - // waiting for next sync - await waitForNextSync("rhsso", syncTime); - - // after sync, ensure group is mirrored - // after sync, ensure user change is mirrorred - LOGGER.info( - `Execute testcase: Rename a user and a group: changes are mirrored in RHDH but permissions should be broken after the sync`, - ); - await common.keycloakLogin( - constants.RHSSO76_USERS["admin"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - await common.CheckUserIsShowingInCatalog([ - (await rhssoHelper.getRHSSOUserDisplayName( - constants.RHSSO76_USERS["user_2"], - )) + " Renamed", - ]); - await common.CheckGroupIsShowingInCatalog([ - constants.RHSSO76_GROUPS["group_2"].name + "_renamed", - ]); - - await uiHelper.openSidebar("Settings"); - await common.signOut(); - - await common.keycloakLogin( - constants.RHSSO76_USERS["user_2"].username, - constants.RHSSO76_DEFAULT_PASSWORD, - ); - - // users permission based on that group will be defaulted to read-only - // expect user not to see catalog entities - await page.goto("/"); - const navMyGroup = page.locator(`nav a:has-text("My Group")`); - await expect(navMyGroup).toHaveCount(0); - - // update the policy with the new group name - await replaceInRBACPolicyFileConfigMap( - "rbac-policy", - constants.AUTH_PROVIDERS_NAMESPACE, - constants.RHSSO76_GROUPS["group_2"].name, - constants.RHSSO76_GROUPS["group_2"].name + "_renamed", - ); - - await uiHelper.openSidebar("Settings"); - // user should see the entities again - await expect(async () => { - await page.reload(); - LOGGER.info( - "Reloading page, permission should be updated automatically.", + test(`${version} - remove a user from RHDH`, async () => { + test.setTimeout(300 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); + + await common.UnregisterUserEntityFromCatalog( + constants.RHSSO76_USERS["user_4"].username, + constants.STATIC_API_TOKEN, ); - await expect(page.locator(`nav a:has-text("My Group")`)).toBeVisible({ - timeout: 2000, + + await expect(async () => { + expect( + await common.CheckUserIsIngestedInCatalog( + [ + rhssoHelper.getRHSSOUserDisplayName( + constants.RHSSO76_USERS["user_4"], + ), + ], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, }); - }).toPass({ - intervals: [5_000, 10_000], - timeout: 120 * 1000, + + // expect entity is not found in rhdh + const loginSucceded = await common.keycloakLogin( + constants.RHSSO76_USERS["user_4"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + expect(loginSucceded).toContain("Login successful"); + + await uiHelper.verifyAlertErrorMessage( + /Login failed; caused by Error: Failed to sign-in, unable to resolve user identity/gm, + ); + + // clear user sessions + await rhssoHelper.clearUserSessions( + constants.RHSSO76_USERS["user_4"].username, + constants.AUTH_PROVIDERS_REALM_NAME, + ); + + await waitForNextSync("rhsso", syntTime); + + // user_4 should login + await common.keycloakLogin( + constants.RHSSO76_USERS["user_4"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + + await uiHelper.openSidebar("Settings"); + await common.signOut(); }); - await uiHelper.openSidebar("My Group"); - await uiHelper.verifyHeading( - constants.RHSSO76_GROUPS["group_2"].name + "_renamed", - ); + test(`${version} - remove a group from RHDH`, async () => { + test.setTimeout(300 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); - await uiHelper.openSidebar("Settings"); - await common.signOut(); + await common.UnregisterGroupEntityFromCatalog( + constants.RHSSO76_GROUPS["group_3"].name, + constants.STATIC_API_TOKEN, + ); + + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.RHSSO76_GROUPS["group_3"].name], + constants.STATIC_API_TOKEN, + ), + ).toBe(false); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + + await waitForNextSync("rhsso", syntTime); + + // after sync, ensure group is created again and memembers can login + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.RHSSO76_GROUPS["group_3"].name], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + }); + + test(`${version} - rename a user and a group`, async () => { + test.setTimeout(300 * 1000); + LOGGER.info(`Executing testcase: ${test.info().title}`); + + await rhssoHelper.updateUser(usersCreated["user_2"].id, { + lastName: constants.RHSSO76_USERS["user_2"].lastName + " Renamed", + emailVerified: true, + email: constants.RHSSO76_USERS["user_2"].username + "@rhdh.com", + }); + + await rhssoHelper.updateGruop(groupsCreated["group_2"].id, { + name: constants.RHSSO76_GROUPS["group_2"].name + "_renamed", + }); + + // waiting for next sync + await waitForNextSync("rhsso", syntTime); + + // after sync, ensure group is mirrored + // after sync, ensure user change is mirrorred + await expect(async () => { + expect( + await common.CheckGroupIsIngestedInCatalog( + [constants.RHSSO76_GROUPS["group_2"].name + "_renamed"], + constants.STATIC_API_TOKEN, + ), + ).toBe(true); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 60 * 1000, + }); + + await common.keycloakLogin( + constants.RHSSO76_USERS["user_2"].username, + constants.RHSSO76_DEFAULT_PASSWORD, + ); + + // users permission based on that group will be defaulted to read-only + // expect user not to see catalog entities + await expect(async () => { + await page.goto("/"); + const navMyGroup = page.locator(`nav a:has-text("My Group")`); + await expect(navMyGroup).toHaveCount(0); + }).toPass({ + intervals: [2_000, 5_000], + timeout: 30 * 1000, + }); + + // update the policy with the new group name + await replaceInRBACPolicyFileConfigMap( + "rbac-policy", + constants.AUTH_PROVIDERS_NAMESPACE, + constants.RHSSO76_GROUPS["group_2"].name, + constants.RHSSO76_GROUPS["group_2"].name + "_renamed", + ); + + await uiHelper.openSidebar("Settings"); + // user should see the entities again + await expect(async () => { + await page.reload(); + LOGGER.info( + "Reloading page, permission should be updated automatically.", + ); + await expect(page.locator(`nav a:has-text("My Group")`)).toBeVisible({ + timeout: 2000, + }); + }).toPass({ + intervals: [5_000, 10_000], + timeout: 120 * 1000, + }); + + await uiHelper.openSidebar("My Group"); + await uiHelper.verifyHeading( + constants.RHSSO76_GROUPS["group_2"].name + "_renamed", + ); + + await uiHelper.openSidebar("Settings"); + await common.signOut(); + }); + + test.afterEach(async () => { + if (test.info().status !== test.info().expectedStatus) { + const prefix = `${test.info().testId}_${test.info().retry}`; + LOGGER.info(`Dumping logs with prefix ${prefix}`); + await dumpAllPodsLogs(prefix, constants.LOGS_FOLDER); + await dumpRHDHUsersAndGroups(prefix, constants.LOGS_FOLDER); + mustSync = true; + } + }); + + test.beforeEach(async () => { + test.setTimeout(120 * 1000); + if (test.info().retry > 0 || mustSync) { + LOGGER.info( + `Waiting for sync. Retry #${test.info().retry}. Needed sync after failure: ${mustSync}.`, + ); + await waitForNextSync("rhsso", syntTime); + mustSync = false; + } + }); }); -}); +} diff --git a/e2e-tests/playwright/e2e/authProviders/setup-environment.spec.ts b/e2e-tests/playwright/e2e/authProviders/setup-environment.spec.ts index eabc605ee0..46ea1bc43d 100644 --- a/e2e-tests/playwright/e2e/authProviders/setup-environment.spec.ts +++ b/e2e-tests/playwright/e2e/authProviders/setup-environment.spec.ts @@ -8,9 +8,8 @@ import { KubeClient } from "../../utils/kube-client"; test.describe("Setup namespace and configure environment for RHDH", () => { test("Create namespace", async () => { - await new KubeClient().createNamespaceIfNotExists( - constants.AUTH_PROVIDERS_NAMESPACE, - ); + const k = new KubeClient(); + await k.createNamespaceIfNotExists(constants.AUTH_PROVIDERS_NAMESPACE); }); test("Create rbac-policy configMap", async () => { diff --git a/e2e-tests/playwright/utils/api-helper.ts b/e2e-tests/playwright/utils/api-helper.ts index f7dd3d054f..c75edddb22 100644 --- a/e2e-tests/playwright/utils/api-helper.ts +++ b/e2e-tests/playwright/utils/api-helper.ts @@ -1,4 +1,6 @@ import { request, APIResponse, expect } from "@playwright/test"; +import * as authProvidersConstants from "./authenticationProviders/constants"; +import { GroupEntity, UserEntity } from "@backstage/catalog-model"; import { GITHUB_API_ENDPOINTS } from "./api-endpoints"; type FetchOptions = { @@ -13,6 +15,8 @@ type FetchOptions = { export class APIHelper { private static githubAPIVersion = "2022-11-28"; + private staticToken: string; + useStaticToken = false; static async githubRequest( method: string, @@ -152,4 +156,141 @@ export class APIHelper { }; return headers; } + + async UseStaticToken(token: string) { + this.useStaticToken = true; + this.staticToken = "Bearer " + token; + } + + static async APIRequestWithStaticToken( + method: string, + url: string, + staticToken: string, + body?: string | object, + ): Promise { + const context = await request.newContext(); + const options = { + method: method, + headers: { + Accept: "application/json", + Authorization: `${staticToken}`, + }, + }; + + if (body) { + options["data"] = body; + } + + const response = await context.fetch(url, options); + return response; + } + + async getAllCatalogUsersFromAPI() { + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/entities/by-query?orderField=metadata.name%2Casc&filter=kind%3Duser`; + const token = this.useStaticToken ? this.staticToken : ""; + const response = await APIHelper.APIRequestWithStaticToken( + "GET", + url, + token, + ); + return response.json(); + } + + async getAllCatalogLocationsFromAPI() { + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/entities/by-query?orderField=metadata.name%2Casc&filter=kind%3Dlocation`; + const token = this.useStaticToken ? this.staticToken : ""; + const response = await APIHelper.APIRequestWithStaticToken( + "GET", + url, + token, + ); + return response.json(); + } + + async getAllCatalogGroupsFromAPI() { + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/entities/by-query?orderField=metadata.name%2Casc&filter=kind%3Dgroup`; + const token = this.useStaticToken ? this.staticToken : ""; + const response = await APIHelper.APIRequestWithStaticToken( + "GET", + url, + token, + ); + return response.json(); + } + + async getGroupEntityFromAPI(group: string) { + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/entities/by-name/group/default/${group}`; + const token = this.useStaticToken ? this.staticToken : ""; + const response = await APIHelper.APIRequestWithStaticToken( + "GET", + url, + token, + ); + return response.json(); + } + + async getCatalogUserFromAPI(user: string) { + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/entities/by-name/user/default/${user}`; + const token = this.useStaticToken ? this.staticToken : ""; + const response = await APIHelper.APIRequestWithStaticToken( + "GET", + url, + token, + ); + return response.json(); + } + + async deleteUserEntityFromAPI(user: string) { + const r: UserEntity = await this.getCatalogUserFromAPI(user); + if (!r.metadata || !r.metadata.uid) { + return; + } + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/entities/by-uid/${r.metadata.uid}`; + const token = this.useStaticToken ? this.staticToken : ""; + const response = await APIHelper.APIRequestWithStaticToken( + "DELETE", + url, + token, + ); + return response.statusText; + } + + async getCatalogGroupFromAPI(group: string) { + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/entities/by-name/group/default/${group}`; + const token = this.useStaticToken ? this.staticToken : ""; + const response = await APIHelper.APIRequestWithStaticToken( + "GET", + url, + token, + ); + return response.json(); + } + + async deleteGroupEntityFromAPI(group: string) { + const r: GroupEntity = await this.getCatalogGroupFromAPI(group); + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/entities/by-uid/${r.metadata.uid}`; + const token = this.useStaticToken ? this.staticToken : ""; + const response = await APIHelper.APIRequestWithStaticToken( + "DELETE", + url, + token, + ); + return response.statusText; + } + + async scheduleEntityRefreshFromAPI( + entity: string, + kind: string, + token: string, + ) { + const url = `${authProvidersConstants.AUTH_PROVIDERS_BASE_URL}/api/catalog/refresh`; + const reqBody = { entityRef: `${kind}:default/${entity}` }; + const responseRefresh = await APIHelper.APIRequestWithStaticToken( + "POST", + url, + token, + reqBody, + ); + return responseRefresh.status(); + } } diff --git a/e2e-tests/playwright/utils/authenticationProviders/constants.ts b/e2e-tests/playwright/utils/authenticationProviders/constants.ts index 29f6109795..295d4331a7 100644 --- a/e2e-tests/playwright/utils/authenticationProviders/constants.ts +++ b/e2e-tests/playwright/utils/authenticationProviders/constants.ts @@ -3,45 +3,28 @@ import GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupR import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import { Group, User } from "@microsoft/microsoft-graph-types"; +// required by RHSSO export const RHSSO76_ADMIN_USERNAME = process.env.RHSSO76_ADMIN_USERNAME; export const RHSSO76_ADMIN_PASSWORD = process.env.RHSSO76_ADMIN_PASSWORD; export const RHSSO76_DEFAULT_PASSWORD = process.env.RHSSO76_DEFAULT_PASSWORD; export const RHSSO76_URL = process.env.RHSSO76_URL; export const RHSSO76_CLIENT_SECRET = process.env.RHSSO76_CLIENT_SECRET; -export const AZURE_LOGIN_USERNAME = process.env.AZURE_LOGIN_USERNAME; -export const AZURE_LOGIN_PASSWORD = process.env.AZURE_LOGIN_PASSWORD; -export const AUTH_PROVIDERS_AZURE_CLIENT_ID = - process.env.AUTH_PROVIDERS_AZURE_CLIENT_ID; -export const AUTH_PROVIDERS_AZURE_CLIENT_SECRET = - process.env.AUTH_PROVIDERS_AZURE_CLIENT_SECRET; -export const AUTH_PROVIDERS_AZURE_TENANT_ID = - process.env.AUTH_PROVIDERS_AZURE_TENANT_ID; -export const AUTH_PROVIDERS_REALM_NAME = process.env.AUTH_PROVIDERS_REALM_NAME; export const RHSSO76_CLIENTID = process.env.RHSSO76_CLIENT_ID; -export const AUTH_PROVIDERS_GH_ORG_NAME = - process.env.AUTH_PROVIDERS_GH_ORG_NAME; -export const AUTH_ORG_APP_ID = process.env.AUTH_ORG_APP_ID; -export const AUTH_ORG_CLIENT_ID = process.env.AUTH_ORG_CLIENT_ID; -export const AUTH_ORG_CLIENT_SECRET = process.env.AUTH_ORG_CLIENT_SECRET; -export const AUTH_ORG1_PRIVATE_KEY = process.env.AUTH_ORG1_PRIVATE_KEY; -export const AUTH_ORG_PK = process.env.AUTH_ORG_PK; -export const AUTH_ORG_WEBHOOK_SECRET = process.env.AUTH_ORG_WEBHOOK_SECRET; -export const AUTH_PROVIDERS_NAMESPACE = process.env.AUTH_PROVIDERS_NAMESPACE; -export const AUTH_PROVIDERS_RELEASE = process.env.AUTH_PROVIDERS_RELEASE; -export const AUTH_PROVIDERS_CHART = process.env.AUTH_PROVIDERS_CHART; -export const CHART_VERSION = process.env.CHART_VERSION; -export const QUAY_REPO = process.env.QUAY_REPO; -export const TAG_NAME = process.env.TAG_NAME; +export const RHSSO76_METADATA_URL = + "https://keycloak-rhsso.rhdh-pr-os-a9805650830b22c3aee243e51d79565d-0000.us-east.containers.appdomain.cloud/auth/realms/authProviders"; -export const AUTH_PROVIDERS_VALUES_FILE = - "../.ibm/pipelines/value_files/values_showcase-auth-providers.yaml"; -export const AUTH_PROVIDERS_POD_STRING = - AUTH_PROVIDERS_RELEASE + "-" + AUTH_PROVIDERS_CHART.split("/")[1]; -export const AUTH_PROVIDERS_BASE_URL = `https://${AUTH_PROVIDERS_RELEASE}-backstage-${AUTH_PROVIDERS_NAMESPACE}.${process.env.K8S_CLUSTER_ROUTER_BASE}`; +export const RHBK_ADMIN_USERNAME = process.env.RHBK_ADMIN_USERNAME; +export const RHBK_ADMIN_PASSWORD = process.env.RHBK_ADMIN_PASSWORD; +export const RHBK_DEFAULT_PASSWORD = process.env.RHBK_DEFAULT_PASSWORD; +export const RHBK_URL = process.env.RHBK_URL; +export const RHBK_CLIENT_SECRET = process.env.RHBK_CLIENT_SECRET; +export const RHBK_CLIENTID = process.env.RHBK_CLIENT_ID; +export const RHBK_METADATA_URL = + "https://rhbk-rhbk.rhdh-pr-os-a9805650830b22c3aee243e51d79565d-0000.us-east.containers.appdomain.cloud/realms/authProviders"; -export const GH_USER_PASSWORD = process.env.GH_USER_PASSWORD; -export const JDOE_NEW_EMAIL = "jenny-doe-new-email@example.com"; -export const AZURE_LOGIN_FIRSTNAME = "QE RHDH Testing Admin"; +export const AUTH_PROVIDERS_REALM_NAME = process.env.AUTH_PROVIDERS_REALM_NAME + ? process.env.AUTH_PROVIDERS_REALM_NAME + : "authProviders"; export const RHSSO76_GROUPS: { [key: string]: GroupRepresentation } = { group_1: { @@ -60,11 +43,9 @@ export const RHSSO76_GROUPS: { [key: string]: GroupRepresentation } = { name: "rhsso_group_location_reader", }, }; - export const RHSSO76_NESTED_GROUP: GroupRepresentation = { name: "rhsso_group_nested", }; - export const RHSSO76_USERS: { [key: string]: UserRepresentation } = { admin: { username: "rhsso_admin", @@ -179,7 +160,6 @@ export const RHSSO76_USERS: { [key: string]: UserRepresentation } = { ], }, }; - export const RHSSO76_CLIENT: ClientRepresentation = { clientId: RHSSO76_CLIENTID, redirectUris: ["*", "/*"], @@ -191,18 +171,27 @@ export const RHSSO76_CLIENT: ClientRepresentation = { implicitFlowEnabled: true, }; +// required by azure +export const AZURE_LOGIN_USERNAME = process.env.AZURE_LOGIN_USERNAME; +export const AZURE_LOGIN_PASSWORD = process.env.AZURE_LOGIN_PASSWORD; +export const AUTH_PROVIDERS_AZURE_CLIENT_ID = + process.env.AUTH_PROVIDERS_AZURE_CLIENT_ID; +export const AUTH_PROVIDERS_AZURE_CLIENT_SECRET = + process.env.AUTH_PROVIDERS_AZURE_CLIENT_SECRET; +export const AUTH_PROVIDERS_AZURE_TENANT_ID = + process.env.AUTH_PROVIDERS_AZURE_TENANT_ID; +export const JDOE_NEW_EMAIL = "jenny-doe-new-email@example.com"; +export const AZURE_LOGIN_FIRSTNAME = "QE RHDH Testing Admin"; export const MSGRAPH_SETTINGS: AppSettings = { clientId: AUTH_PROVIDERS_AZURE_CLIENT_ID, clientSecret: AUTH_PROVIDERS_AZURE_CLIENT_SECRET, tenantId: AUTH_PROVIDERS_AZURE_TENANT_ID, }; - export interface AppSettings { clientId: string; clientSecret: string; tenantId: string; } - export const MSGRAPH_USERS: { [key: string]: User } = { admin: { accountEnabled: true, @@ -293,7 +282,6 @@ export const MSGRAPH_USERS: { [key: string]: User } = { }, }, }; - export const MSGRAPH_GROUPS: { [key: string]: Group } = { group_1: { description: "Group 1 for RHDH test automation - DO NOT USE/EDIT/DELETE", @@ -352,7 +340,6 @@ export const MSGRAPH_GROUPS: { [key: string]: Group } = { securityEnabled: true, }, }; - export const GH_TEAMS: { [key: string]: { name: string } } = { team_1: { name: "gh_team_1", @@ -370,16 +357,60 @@ export const GH_TEAMS: { [key: string]: { name: string } } = { name: "gh_team_location_reader", }, }; - -export const GH_USERS: { [key: string]: { name: string } } = { +export const GH_USERS: { + [key: string]: { name: string; displayName: string }; +} = { user_1: { name: "rhdhqeauth1", + displayName: "RHDH QE User 1", }, admin: { name: "rhdhqeauthadmin", + displayName: "RHDH QE Admin", }, }; +// required by Github +export const AUTH_PROVIDERS_GH_ORG_NAME = + process.env.AUTH_PROVIDERS_GH_ORG_NAME; +export const AUTH_ORG_APP_ID = process.env.AUTH_ORG_APP_ID; +export const AUTH_ORG_CLIENT_ID = process.env.AUTH_ORG_CLIENT_ID; +export const AUTH_ORG_CLIENT_SECRET = process.env.AUTH_ORG_CLIENT_SECRET; +export const AUTH_ORG1_PRIVATE_KEY = process.env.AUTH_ORG1_PRIVATE_KEY; +export const AUTH_ORG_PK = process.env.AUTH_ORG_PK; +export const AUTH_ORG_WEBHOOK_SECRET = process.env.AUTH_ORG_WEBHOOK_SECRET; +export const GH_USER_PASSWORD = process.env.GH_USER_PASSWORD; +export const AUTH_PROVIDERS_GH_USER_2FA = + process.env.AUTH_PROVIDERS_GH_USER_2FA; +export const AUTH_PROVIDERS_GH_ADMIN_2FA = + process.env.AUTH_PROVIDERS_GH_ADMIN_2FA; + +// required by all auth scenarios +export const AUTH_PROVIDERS_NAMESPACE = process.env.AUTH_PROVIDERS_NAMESPACE + ? process.env.AUTH_PROVIDERS_NAMESPACE + : "showcase-auth-providers"; +export const STATIC_API_TOKEN = process.env.STATIC_API_TOKEN + ? process.env.STATIC_API_TOKEN + : "somecicdtoken"; +export const AUTH_PROVIDERS_RELEASE = process.env.AUTH_PROVIDERS_RELEASE + ? process.env.AUTH_PROVIDERS_RELEASE + : "rhdh-auth-providers"; +export const AUTH_PROVIDERS_CHART = process.env.AUTH_PROVIDERS_CHART + ? process.env.AUTH_PROVIDERS_CHART + : "rhdh-chart/backstage"; +export const CHART_VERSION = process.env.CHART_VERSION + ? process.env.CHART_VERSION + : "2.15.2"; +export const QUAY_REPO = process.env.QUAY_REPO + ? process.env.QUAY_REPO + : "janus-idp/backstage-showcase"; +export const TAG_NAME = process.env.TAG_NAME; +export const LOGS_FOLDER = process.env.LOGS_FOLDER + ? process.env.LOGS_FOLDER + : "/tmp/backstage-showcase/e2e-tests/auth-providers-logs"; +export const AUTH_PROVIDERS_VALUES_FILE = + "../.ibm/pipelines/value_files/values_showcase-auth-providers.yaml"; +export const AUTH_PROVIDERS_BASE_URL = `https://${AUTH_PROVIDERS_RELEASE}-backstage-${AUTH_PROVIDERS_NAMESPACE}.${process.env.K8S_CLUSTER_ROUTER_BASE}`; export const RBAC_POLICY_ROLES: string = ` p, role:default/admin, catalog-entity, read, allow p, role:default/admin, catalog-entity, update, allow diff --git a/e2e-tests/playwright/utils/authenticationProviders/github-helper.ts b/e2e-tests/playwright/utils/authenticationProviders/github-helper.ts index 9cb7789ef5..bebd789705 100644 --- a/e2e-tests/playwright/utils/authenticationProviders/github-helper.ts +++ b/e2e-tests/playwright/utils/authenticationProviders/github-helper.ts @@ -19,18 +19,13 @@ export async function setupGithubEnvironment() { } // recreate them for (const key in constants.GH_TEAMS) { + //TBD: improve nested team creation await createTeam( constants.GH_TEAMS[key].name, constants.AUTH_PROVIDERS_GH_ORG_NAME, ); } - await setParentTeam( - constants.GH_TEAMS["team_3"].name, - constants.AUTH_PROVIDERS_GH_ORG_NAME, - constants.GH_TEAMS["team_2"].name, - ); - await addMemberToTeam( constants.GH_TEAMS["team_3"].name, constants.AUTH_PROVIDERS_GH_ORG_NAME, @@ -52,6 +47,12 @@ export async function setupGithubEnvironment() { constants.GH_USERS["admin"].name, ); + await setParentTeam( + constants.GH_TEAMS["team_3"].name, + constants.AUTH_PROVIDERS_GH_ORG_NAME, + constants.GH_TEAMS["team_2"].name, + ); + await helper.ensureNewPolicyConfigMapExists( "rbac-policy", constants.AUTH_PROVIDERS_NAMESPACE, @@ -83,19 +84,6 @@ export async function renameTeam(team: string, org: string, newname: string) { }); } -export async function setParentTeam( - team: string, - org: string, - parentTeam: string, -) { - const parentTeamObj = await getTeamByName(parentTeam, org); - return await octokit.rest.teams.updateInOrg({ - team_slug: team, - org, - parentTeamId: parentTeamObj.data.id, - }); -} - export async function deleteTeam(team: string, org: string) { try { LOGGER.info(`Deleting team from github ${team} in org ${org}`); @@ -110,12 +98,35 @@ export async function deleteTeam(team: string, org: string) { } } +export async function setParentTeam( + team: string, + org: string, + parentTeam: string, +) { + try { + const parentTeamObj = await getTeamByName(parentTeam, org); + LOGGER.info( + `Adding parent team ${JSON.stringify(parentTeamObj.data.name)} to team ${team}`, + ); + const r = await octokit.rest.teams.updateInOrg({ + team_slug: team, + org, + parent_team_id: parentTeamObj.data.id, + }); + return r; + } catch (e) { + LOGGER.info(`Error setting github parent team: ${JSON.stringify(e)}`); + } +} + export async function createTeam(team: string, org: string) { - return await octokit.rest.teams.create({ + const r = await octokit.rest.teams.create({ name: team, org, privacy: "closed", }); + LOGGER.info(`Creation team response: ${JSON.stringify(r.status)}`); + return r; } export async function listTeams(org: string) { diff --git a/e2e-tests/playwright/utils/authenticationProviders/msgraph-helper.ts b/e2e-tests/playwright/utils/authenticationProviders/msgraph-helper.ts index 9e1b6d5374..cbb55b9118 100644 --- a/e2e-tests/playwright/utils/authenticationProviders/msgraph-helper.ts +++ b/e2e-tests/playwright/utils/authenticationProviders/msgraph-helper.ts @@ -448,3 +448,7 @@ export async function setupMicrosoftEntraIDEnvironment(): Promise<{ groupsCreated, }; } + +export function formatUPNToEntity(user: string) { + return user.replace("@", "_"); +} diff --git a/e2e-tests/playwright/utils/authenticationProviders/rh-sso-helper.ts b/e2e-tests/playwright/utils/authenticationProviders/rh-sso-helper.ts index 4dfcecffba..dc9cfb9857 100644 --- a/e2e-tests/playwright/utils/authenticationProviders/rh-sso-helper.ts +++ b/e2e-tests/playwright/utils/authenticationProviders/rh-sso-helper.ts @@ -8,216 +8,242 @@ import { ConnectionConfig } from "@keycloak/keycloak-admin-client/lib/client"; import { Credentials } from "@keycloak/keycloak-admin-client/lib/utils/auth"; import * as helper from "../helper"; -let kcAdminClient: KcAdminClient | undefined; - -export const CONNECTION_CONFIG: ConnectionConfig = { - baseUrl: constants.RHSSO76_URL, - realmName: constants.AUTH_PROVIDERS_REALM_NAME, -}; - -const cred: Credentials = { - clientSecret: constants.RHSSO76_CLIENT_SECRET, - grantType: "client_credentials", - clientId: constants.RHSSO76_CLIENTID, - scopes: ["openid", "profile"], -}; - -export async function initializeRHSSOClient( - connectionConfig: ConnectionConfig, -) { - // Ensure settings isn't null - if (!connectionConfig) { - LOGGER.error(`RHSSO config cannot be undefined`); - throw new Error("Config cannot be undefined"); - } - LOGGER.info(`Initializing RHSSO client`); - kcAdminClient = new KcAdminClient(connectionConfig); - await kcAdminClient.auth(cred); - setInterval(() => kcAdminClient.auth(cred), 58 * 1000); -} - -export async function setupRHSSOEnvironment(): Promise<{ - usersCreated: Map; - groupsCreated: Map; -}> { - LOGGER.info("Setting up RHSSO environment"); - const usersCreated = new Map(); - const groupsCreated = new Map(); - - try { - const realmSearch = await kcAdminClient.realms.findOne({ - realm: constants.AUTH_PROVIDERS_REALM_NAME, - }); - expect(realmSearch).not.toBeNull(); - - // Override client configuration for all further requests: - kcAdminClient.setConfig({ - realmName: constants.AUTH_PROVIDERS_REALM_NAME, - }); - - //cleanup existing users - const users = await kcAdminClient.users.find(); - for (const user of users) { - await kcAdminClient.users.del({ id: user.id! }); - } - - //cleanup existing groups - const groups = await kcAdminClient.groups.find(); - for (const group of groups) { - await kcAdminClient.groups.del({ id: group.id! }); +export class RHSSOHelper { + kcAdminClient: KcAdminClient | undefined; + connectionConfig: ConnectionConfig; + cred: Credentials; + version: string; + + constructor(version: string) { + this.version = version; + if (version == "RHSSO") { + this.connectionConfig = { + baseUrl: constants.RHSSO76_URL, + realmName: constants.AUTH_PROVIDERS_REALM_NAME, + }; + this.cred = { + clientSecret: constants.RHSSO76_CLIENT_SECRET, + grantType: "client_credentials", + clientId: constants.RHSSO76_CLIENTID, + scopes: ["openid", "profile"], + }; + } else if (version == "RHBK") { + this.connectionConfig = { + baseUrl: constants.RHBK_URL, + realmName: constants.AUTH_PROVIDERS_REALM_NAME, + }; + this.cred = { + clientSecret: constants.RHBK_CLIENT_SECRET, + grantType: "client_credentials", + clientId: constants.RHBK_CLIENTID, + scopes: ["openid", "profile"], + }; } + } - for (const key in constants.RHSSO76_GROUPS) { - const group = constants.RHSSO76_GROUPS[key]; - const newGroup = await kcAdminClient.groups.create(group); - groupsCreated[key] = newGroup; + async initializeRHSSOClient() { + // Ensure settings isn't null + if (!this.connectionConfig) { + LOGGER.error(`${this.version} config cannot be undefined`); + throw new Error("Config cannot be undefined"); } + LOGGER.info(`Initializing ${this.version} client`); + this.kcAdminClient = new KcAdminClient(this.connectionConfig); + await this.kcAdminClient.auth(this.cred); + setInterval(() => this.kcAdminClient.auth(this.cred), 58 * 1000); + } - for (const key in constants.RHSSO76_USERS) { - const user = constants.RHSSO76_USERS[key]; - const newUser = await kcAdminClient.users.create(user); - usersCreated[key] = newUser; + async setupRHSSOEnvironment(): Promise<{ + usersCreated: Map; + groupsCreated: Map; + }> { + LOGGER.info(`Setting up ${this.version} environment`); + const usersCreated = new Map(); + const groupsCreated = new Map(); + + try { + const realmSearch = await this.kcAdminClient.realms.findOne({ + realm: constants.AUTH_PROVIDERS_REALM_NAME, + }); + expect(realmSearch).not.toBeNull(); + + // Override client configuration for all further requests: + this.kcAdminClient.setConfig({ + realmName: constants.AUTH_PROVIDERS_REALM_NAME, + }); + + //cleanup existing users + const users = await this.kcAdminClient.users.find(); + for (const user of users) { + await this.kcAdminClient.users.del({ id: user.id! }); + } + + //cleanup existing groups + const groups = await this.kcAdminClient.groups.find(); + for (const group of groups) { + await this.kcAdminClient.groups.del({ id: group.id! }); + } + + for (const key in constants.RHSSO76_GROUPS) { + const group = constants.RHSSO76_GROUPS[key]; + const newGroup = await this.kcAdminClient.groups.create(group); + groupsCreated[key] = newGroup; + } + + for (const key in constants.RHSSO76_USERS) { + const user = constants.RHSSO76_USERS[key]; + const newUser = await this.kcAdminClient.users.create(user); + usersCreated[key] = newUser; + } + + const nestedgroup = await this.kcAdminClient.groups.createChildGroup( + { id: groupsCreated["group_2"].id }, + constants.RHSSO76_NESTED_GROUP, + ); + LOGGER.info(JSON.stringify(nestedgroup)); + + await this.kcAdminClient.users.addToGroup({ + id: usersCreated["user_3"].id, + groupId: nestedgroup.id, + }); + + // create rbac policy for created users + await helper.ensureNewPolicyConfigMapExists( + "rbac-policy", + constants.AUTH_PROVIDERS_NAMESPACE, + ); + } catch (e) { + LOGGER.log({ + level: "error", + message: `${this.version} setup failed:`, + dump: JSON.stringify(e), + }); + throw new Error(`${this.version} setup failed: ${JSON.stringify(e)}`); } + return { + usersCreated, + groupsCreated, + }; + } - const nestedgroup = await kcAdminClient.groups.createChildGroup( - { id: groupsCreated["group_2"].id }, - constants.RHSSO76_NESTED_GROUP, - ); - - await kcAdminClient.users.addToGroup({ - id: usersCreated["user_3"].id, - groupId: nestedgroup.id, + async clearUserSessions(username: string, realm: string) { + const usr = await this.kcAdminClient.users.find({ + q: `username:${username}`, + }); + const sessions = await this.kcAdminClient.users.listSessions({ + id: usr[0].id, }); - - // create rbac policy for created users - await helper.ensureNewPolicyConfigMapExists( - "rbac-policy", - constants.AUTH_PROVIDERS_NAMESPACE, - ); - } catch (e) { LOGGER.log({ - level: "error", - message: "RHSSO setup failed:", - dump: JSON.stringify(e), + level: "info", + message: `Clearing ${username} sessions`, + dump: JSON.stringify(sessions.map((s) => s.id)), }); - throw new Error("RHSSO setup failed: " + JSON.stringify(e)); - } - return { - usersCreated, - groupsCreated, - }; -} -export async function clearUserSessions(username: string, realm: string) { - const usr = await kcAdminClient.users.find({ - q: `username:${username}`, - }); - const sessions = await kcAdminClient.users.listSessions({ - id: usr[0].id, - }); - LOGGER.log({ - level: "info", - message: `Clearing ${username} sessions`, - dump: JSON.stringify(sessions.map((s) => s.id)), - }); - - for (const s of sessions) { - await kcAdminClient.realms.removeSession({ - realm: realm, - sessionId: s.id, - }); + for (const s of sessions) { + await this.kcAdminClient.realms.removeSession({ + realm: realm, + sessionId: s.id, + }); + } } -} -export async function updateUser(userId: string, userObj: UserRepresentation) { - try { - LOGGER.info(`Update user ${userId} from RHSSO`); - await kcAdminClient.users.update({ id: userId }, userObj); - } catch (e) { - LOGGER.error(e); - throw e; + async updateUser(userId: string, userObj: UserRepresentation) { + try { + LOGGER.info(`Update user ${userId} from ${this.version}`); + await this.kcAdminClient.users.update({ id: userId }, userObj); + } catch (e) { + LOGGER.error(e); + throw e; + } } -} -export async function updateGruop( - groupId: string, - groupObj: GroupRepresentation, -) { - try { - LOGGER.info(`Update group ${groupId} from RHSSO`); - await kcAdminClient.groups.update({ id: groupId }, groupObj); - } catch (e) { - LOGGER.error(e); - throw e; + async updateGruop(groupId: string, groupObj: GroupRepresentation) { + try { + LOGGER.info(`Update group ${groupId} from ${this.version}`); + await this.kcAdminClient.groups.update({ id: groupId }, groupObj); + } catch (e) { + LOGGER.error(e); + throw e; + } } -} -export async function updateUserEmail(username: string, newEmail: string) { - let jd: UserRepresentation[]; - try { - jd = await kcAdminClient.users.find({ - q: `username:${username}`, - }); - expect(jd.length).toBe(1); - const res = await kcAdminClient.users.update( - { id: jd[0].id }, - { email: newEmail }, - ); - LOGGER.log({ - level: "info", - message: `Updated user: ${username}: `, - dump: JSON.stringify(res), - }); - } catch (e) { - LOGGER.log({ - level: "info", - message: "RHSSO update email failed:", - dump: JSON.stringify(e), - }); - throw new Error("Cannot update user: " + JSON.stringify(e)); + async updateUserEmail(username: string, newEmail: string) { + let jd: UserRepresentation[]; + try { + jd = await this.kcAdminClient.users.find({ + q: `username:${username}`, + }); + expect(jd.length).toBe(1); + const res = await this.kcAdminClient.users.update( + { id: jd[0].id }, + { email: newEmail }, + ); + LOGGER.log({ + level: "info", + message: `Updated user: ${username}: `, + dump: JSON.stringify(res), + }); + } catch (e) { + LOGGER.log({ + level: "info", + message: `${this.version} update email failed:`, + dump: JSON.stringify(e), + }); + throw new Error("Cannot update user: " + JSON.stringify(e)); + } } -} -export async function deleteUser(id: string) { - try { - LOGGER.info(`Deleting user ${id} from RHSSO`); - await kcAdminClient.users.del({ id: id }); - } catch (e) { - LOGGER.error(e); - throw e; + async deleteUser(id: string) { + try { + LOGGER.info(`Deleting user ${id} from ${this.version}`); + await this.kcAdminClient.users.del({ id: id }); + } catch (e) { + LOGGER.error(e); + throw e; + } } -} -export async function removeUserFromGroup(userId: string, groupId: string) { - try { - LOGGER.info(`Remove user ${userId} from group ${groupId} from RHSSO`); - await kcAdminClient.users.delFromGroup({ id: userId, groupId: groupId }); - } catch (e) { - LOGGER.error(e); - throw e; + async removeUserFromGroup(userId: string, groupId: string) { + try { + LOGGER.info( + `Remove user ${userId} from group ${groupId} from ${this.version}`, + ); + await this.kcAdminClient.users.delFromGroup({ + id: userId, + groupId: groupId, + }); + } catch (e) { + LOGGER.error(e); + throw e; + } } -} -export async function addUserToGroup(userId: string, groupId: string) { - try { - LOGGER.info(`Add user ${userId} from group ${groupId} from RHSSO`); - await kcAdminClient.users.addToGroup({ id: userId, groupId: groupId }); - } catch (e) { - LOGGER.error(e); - throw e; + async addUserToGroup(userId: string, groupId: string) { + try { + LOGGER.info( + `Add user ${userId} from group ${groupId} from ${this.version}`, + ); + await this.kcAdminClient.users.addToGroup({ + id: userId, + groupId: groupId, + }); + } catch (e) { + LOGGER.error(e); + throw e; + } } -} -export async function deleteGroup(groupId: string) { - try { - LOGGER.info(`Deleting group ${groupId} from RHSSO`); - await kcAdminClient.groups.del({ id: groupId }); - } catch (e) { - LOGGER.error(e); - throw e; + async deleteGroup(groupId: string) { + try { + LOGGER.info(`Deleting group ${groupId} from ${this.version}`); + await this.kcAdminClient.groups.del({ id: groupId }); + } catch (e) { + LOGGER.error(e); + throw e; + } } -} -export function getRHSSOUserDisplayName(user: UserRepresentation) { - return user.firstName + " " + user.lastName; + getRHSSOUserDisplayName(user: UserRepresentation) { + return user.firstName + " " + user.lastName; + } } diff --git a/e2e-tests/playwright/utils/common.ts b/e2e-tests/playwright/utils/common.ts index b9d46ca9e3..8ab23743ac 100644 --- a/e2e-tests/playwright/utils/common.ts +++ b/e2e-tests/playwright/utils/common.ts @@ -1,6 +1,9 @@ import { UIhelper } from "./ui-helper"; import { authenticator } from "otplib"; import { test, Browser, expect, Page, TestInfo } from "@playwright/test"; +import { APIHelper } from "./api-helper"; +import { GroupEntity, UserEntity } from "@backstage/catalog-model"; +import { LOGGER } from "./logger"; import { SETTINGS_PAGE_COMPONENTS } from "../support/pageObjects/page-obj"; import { WAIT_OBJECTS } from "../support/pageObjects/global-obj"; import path from "path"; @@ -194,111 +197,144 @@ export class Common { } async keycloakLogin(username: string, password: string) { + let popup: Page; + this.page.once("popup", (asyncnewPage) => { + popup = asyncnewPage; + }); + await this.page.goto("/"); await this.page.waitForSelector('p:has-text("Sign in using OIDC")'); await this.uiHelper.clickButton("Sign In"); - return await new Promise((resolve) => { - this.page.once("popup", async (popup) => { - await popup.waitForLoadState(); - if (popup.url().startsWith(process.env.BASE_URL)) { - // an active rhsso session is already logged in and the popup will automatically close - resolve("Already logged in"); + // Wait for the popup to appear + await expect(async () => { + await popup.waitForLoadState("domcontentloaded"); + expect(popup).toBeTruthy(); + }).toPass({ + intervals: [5_000, 10_000], + timeout: 20 * 1000, + }); + + if (popup.url().startsWith(process.env.BASE_URL)) { + // an active rhsso session is already logged in and the popup will automatically close + return "Already logged in"; + } else { + try { + await popup.locator("#username").click(); + await popup.locator("#username").fill(username); + await popup.locator("#password").fill(password); + await popup.locator("[name=login]").click({ timeout: 5000 }); + await popup.waitForEvent("close", { timeout: 2000 }); + return "Login successful"; + } catch (e) { + const usernameError = popup.locator("id=input-error"); + if (await usernameError.isVisible()) { + await popup.close(); + return "User does not exist"; } else { - await popup.waitForTimeout(3000); - try { - await popup.locator("#username").fill(username); - await popup.locator("#password").fill(password); - await popup.locator("[name=login]").click({ timeout: 5000 }); - await popup.waitForEvent("close", { timeout: 2000 }); - resolve("Login successful"); - } catch (e) { - const usernameError = popup.locator("id=input-error"); - if (await usernameError.isVisible()) { - await popup.close(); - resolve("User does not exist"); - } else { - throw e; - } - } + throw e; } - }); - }); + } + } } - async githubLogin(username: string, password: string) { + async githubLogin(username: string, password: string, twofactor: string) { + let popup: Page; + this.page.once("popup", (asyncnewPage) => { + popup = asyncnewPage; + }); + await this.page.goto("/"); await this.page.waitForSelector('p:has-text("Sign in using GitHub")'); await this.uiHelper.clickButton("Sign In"); - return await new Promise((resolve) => { - this.page.once("popup", async (popup) => { - await popup.waitForLoadState(); - if (popup.url().startsWith(process.env.BASE_URL)) { - // an active rhsso session is already logged in and the popup will automatically close - resolve("Already logged in"); + // Wait for the popup to appear + await expect(async () => { + await popup.waitForLoadState("domcontentloaded"); + expect(popup).toBeTruthy(); + }).toPass({ + intervals: [5_000, 10_000], + timeout: 20 * 1000, + }); + + if (popup.url().startsWith(process.env.BASE_URL)) { + // an active rhsso session is already logged in and the popup will automatically close + return "Already logged in"; + } else { + try { + await popup.locator("#login_field").click({ timeout: 5000 }); + await popup.locator("#login_field").fill(username, { timeout: 5000 }); + await popup.locator("#password").click({ timeout: 5000 }); + await popup.locator("#password").fill(password, { timeout: 5000 }); + await popup.locator("[type='submit']").click({ timeout: 5000 }); + const twofactorcode = authenticator.generate(twofactor); + await popup.locator("#app_totp").click({ timeout: 5000 }); + await popup.locator("#app_totp").fill(twofactorcode, { timeout: 5000 }); + + await popup.waitForEvent("close", { timeout: 20000 }); + return "Login successful"; + } catch (e) { + const authorization = popup.locator("button.js-oauth-authorize-btn"); + if (await authorization.isVisible()) { + authorization.click(); + return "Login successful with app authorization"; } else { - await popup.waitForTimeout(3000); - try { - await popup.locator("#login_field").fill(username); - await popup.locator("#password").fill(password); - await popup.locator("[type='submit']").click({ timeout: 5000 }); - //await this.checkAndReauthorizeGithubApp() - await popup.waitForEvent("close", { timeout: 2000 }); - resolve("Login successful"); - } catch (e) { - const authorization = popup.locator( - "button.js-oauth-authorize-btn", - ); - if (await authorization.isVisible()) { - authorization.click(); - resolve("Login successful with app authorization"); - } else { - throw e; - } - } + throw e; } - }); - }); + } + } } async MicrosoftAzureLogin(username: string, password: string) { + let popup: Page; + this.page.once("popup", (asyncnewPage) => { + popup = asyncnewPage; + }); + await this.page.goto("/"); await this.page.waitForSelector('p:has-text("Sign in using Microsoft")'); await this.uiHelper.clickButton("Sign In"); - return await new Promise((resolve) => { - this.page.once("popup", async (popup) => { - await popup.waitForLoadState(); - if (popup.url().startsWith(process.env.BASE_URL)) { - // an active microsoft session is already logged in and the popup will automatically close - resolve("Already logged in"); + // Wait for the popup to appear + await expect(async () => { + await popup.waitForLoadState("domcontentloaded"); + expect(popup).toBeTruthy(); + }).toPass({ + intervals: [5_000, 10_000], + timeout: 20 * 1000, + }); + + if (popup.url().startsWith(process.env.BASE_URL)) { + // an active microsoft session is already logged in and the popup will automatically close + return "Already logged in"; + } else { + try { + await popup.locator("[name=loginfmt]").click(); + await popup + .locator("[name=loginfmt]") + .fill(username, { timeout: 5000 }); + await popup + .locator('[type=submit]:has-text("Next")') + .click({ timeout: 5000 }); + + await popup.locator("[name=passwd]").click(); + await popup.locator("[name=passwd]").fill(password, { timeout: 5000 }); + await popup + .locator('[type=submit]:has-text("Sign in")') + .click({ timeout: 5000 }); + await popup + .locator('[type=button]:has-text("No")') + .click({ timeout: 15000 }); + return "Login successful"; + } catch (e) { + const usernameError = popup.locator("id=usernameError"); + if (await usernameError.isVisible()) { + return "User does not exist"; } else { - try { - await popup.locator("[name=loginfmt]").fill(username); - await popup - .locator('[type=submit]:has-text("Next")') - .click({ timeout: 5000 }); - - await popup.locator("[name=passwd]").fill(password); - await popup - .locator('[type=submit]:has-text("Sign in")') - .click({ timeout: 5000 }); - await popup - .locator('[type=button]:has-text("No")') - .click({ timeout: 15000 }); - resolve("Login successful"); - } catch (e) { - const usernameError = popup.locator("id=usernameError"); - if (await usernameError.isVisible()) { - resolve("User does not exist"); - } else { - throw e; - } - } + throw e; } - }); - }); + } + } } async GetParentGroupDisplayed(): Promise { @@ -351,28 +387,16 @@ export class Common { }; } - async UnregisterUserEnittyFromCatalog(user: string) { - await this.page.goto("/"); - await this.uiHelper.openSidebar("Catalog"); - await this.uiHelper.selectMuiBox("Kind", "User"); - await this.uiHelper.verifyHeading("All users"); - - await this.uiHelper.clickLink(user); - await this.uiHelper.verifyHeading(user); - - await this.uiHelper.clickUnregisterButtonForDisplayedEntity(); + async UnregisterUserEntityFromCatalog(user: string, apiToken: string) { + const api = new APIHelper(); + api.UseStaticToken(apiToken); + await api.deleteUserEntityFromAPI(user); } - async UnregisterGroupEnittyFromCatalog(group: string) { - await this.page.goto("/"); - await this.uiHelper.openSidebar("Catalog"); - await this.uiHelper.selectMuiBox("Kind", "Group"); - await this.uiHelper.verifyHeading("All groups"); - - await this.uiHelper.clickLink(group); - await this.uiHelper.verifyHeading(group); - - await this.uiHelper.clickUnregisterButtonForDisplayedEntity(); + async UnregisterGroupEntityFromCatalog(group: string, apiToken: string) { + const api = new APIHelper(); + api.UseStaticToken(apiToken); + await api.deleteGroupEntityFromAPI(group); } async CheckGroupIsShowingInCatalog(groups: string[]) { @@ -398,6 +422,46 @@ export class Common { await this.uiHelper.verifyHeading("All user"); await this.uiHelper.verifyCellsInTable(users); } + + async CheckUserIsIngestedInCatalog(users: string[], apiToken: string) { + const api = new APIHelper(); + api.UseStaticToken(apiToken); + const response = await api.getAllCatalogUsersFromAPI(); + LOGGER.info(`Users currently in catalog: ${JSON.stringify(response)}`); + const catalogUsers: UserEntity[] = + response && response.items ? response.items : []; + expect(catalogUsers.length).toBeGreaterThan(0); + const catalogUsersDisplayNames: string[] = catalogUsers.map( + (u) => u.spec.profile.displayName, + ); + LOGGER.info( + `Checking ${JSON.stringify(catalogUsersDisplayNames)} contains users ${JSON.stringify(users)}`, + ); + const hasAllElems = users.every((elem) => + catalogUsersDisplayNames.includes(elem), + ); + return hasAllElems; + } + + async CheckGroupIsIngestedInCatalog(groups: string[], apiToken: string) { + const api = new APIHelper(); + api.UseStaticToken(apiToken); + const response = await api.getAllCatalogGroupsFromAPI(); + LOGGER.info(`Groups currently in catalog: ${JSON.stringify(response)}`); + const catalogGroups: GroupEntity[] = + response && response.items ? response.items : []; + expect(catalogGroups.length).toBeGreaterThan(0); + const catalogGroupsDisplayNames: string[] = catalogGroups.map( + (u) => u.spec.profile.displayName, + ); + LOGGER.info( + `Checking ${JSON.stringify(catalogGroupsDisplayNames)} contains groups ${JSON.stringify(groups)}`, + ); + const hasAllElems = groups.every((elem) => + catalogGroupsDisplayNames.includes(elem), + ); + return hasAllElems; + } } export async function setupBrowser(browser: Browser, testInfo: TestInfo) { diff --git a/e2e-tests/playwright/utils/helm.ts b/e2e-tests/playwright/utils/helm.ts index 8878689ab7..fede425100 100644 --- a/e2e-tests/playwright/utils/helm.ts +++ b/e2e-tests/playwright/utils/helm.ts @@ -47,7 +47,7 @@ export class HelmActions { static async deleteHelmReleaseWithWait(release: string, namespace: string) { LOGGER.info(`Deleting release ${release} in namespace ${namespace}`); const result = await runShellCmd( - `helm uninstall ${release} --wait --timeout 300s -n ${namespace} --ignore-not-found`, + `helm uninstall ${release} --wait --timeout 300s -n ${namespace} || true`, ); LOGGER.log({ level: "info", diff --git a/e2e-tests/playwright/utils/helper.ts b/e2e-tests/playwright/utils/helper.ts index cdb3814914..859c0922c1 100644 --- a/e2e-tests/playwright/utils/helper.ts +++ b/e2e-tests/playwright/utils/helper.ts @@ -4,10 +4,13 @@ import * as constants from "./authenticationProviders/constants"; import { expect } from "@playwright/test"; import { KubeClient } from "./kube-client"; import { V1ConfigMap, V1Secret } from "@kubernetes/client-node"; +import { GroupEntity } from "@backstage/catalog-model"; +import fs from "fs"; +import { APIHelper } from "./api-helper"; export async function runShellCmd(command: string) { return new Promise((resolve) => { - LOGGER.info(`Executing command ${command}`); + //logger.info(`Executing command ${command}`); const process = spawn("/bin/sh", ["-c", command]); let result: string; process.stdout.on("data", (data) => { @@ -17,10 +20,11 @@ export async function runShellCmd(command: string) { result = data; }); process.on("exit", (code) => { - LOGGER.info(`Process ended with exit code ${code}: `); if (code == 0) { resolve(result); + return; } else { + LOGGER.info(`Process failed with code ${code}: ${result}`); throw Error(`Error executing shell command; exit code ${code}`); } }); @@ -41,18 +45,21 @@ export async function upgradeHelmChartWithWait( await deleteHelmReleaseWithWait(release, namespace); LOGGER.info(`Upgrading helm release ${release}`); - const upgradeOutput = await runShellCmd(`helm upgrade \ + const upgradeCMD = `helm upgrade \ -i ${release} ${chart} \ --wait --timeout 300s -n ${namespace} \ --values ${value} \ --version "${chartVersion}" --set upstream.backstage.image.repository="${quayRepo}" --set upstream.backstage.image.tag="${tag}" \ --set global.clusterRouterBase=${process.env.K8S_CLUSTER_ROUTER_BASE} \ - ${flags.join(" ")}`); + ${flags.join(" ")}`; + LOGGER.info(`Running upgrade with command ${upgradeCMD}`); + + const upgradeOutput = await runShellCmd(upgradeCMD); LOGGER.log({ level: "info", message: `Release upgrade returned: `, - dump: upgradeOutput, + dump: upgradeOutput.toString(), }); const configmap = await new KubeClient().getConfigMap( @@ -74,14 +81,14 @@ export async function deleteHelmReleaseWithWait( ) { LOGGER.info(`Deleting release ${release} in namespace ${namespace}`); const result = await runShellCmd( - `helm uninstall ${release} --wait --timeout 300s -n ${namespace} --ignore-not-found`, + `helm uninstall ${release} --wait --timeout 300s -n ${namespace} || true`, ); LOGGER.log({ level: "info", message: `Release delete returned: `, - dump: result, + dump: result.toString(), }); - return result; + return result.toString(); } export async function getLastSyncTimeFromLogs( @@ -98,11 +105,18 @@ export async function getLastSyncTimeFromLogs( try { // TBD: change this to use kube api - const podName = await runShellCmd( - `oc get pods -n ${constants.AUTH_PROVIDERS_NAMESPACE} | awk '{print $1}' | grep '^${constants.AUTH_PROVIDERS_POD_STRING}'`, + const p = await new KubeClient().coreV1Api.listNamespacedPod( + constants.AUTH_PROVIDERS_NAMESPACE, + undefined, + undefined, + undefined, + undefined, + "app.kubernetes.io/component=backstage", ); + const pods = p.body.items.map((pod) => pod.metadata.name); + const log = await runShellCmd( - `oc logs ${podName.trim()} -n ${constants.AUTH_PROVIDERS_NAMESPACE} -c backstage-backend | grep "${searchString}" | tail -n1`, + `oc logs ${pods[0].trim()} -n ${constants.AUTH_PROVIDERS_NAMESPACE} -c backstage-backend | grep "${searchString}" | tail -n1`, ); const syncObj = Date.parse(JSON.parse(log).timestamp); return syncObj; @@ -112,20 +126,21 @@ export async function getLastSyncTimeFromLogs( } } -export async function waitForNextSync(provider: string, syncTime?: number) { +export async function waitForNextSync(provider: string, synTimeOut: number) { + let syncTime: number | null = null; await expect(async () => { - const lastSyncTimeFromLogs = await getLastSyncTimeFromLogs(provider); - if (syncTime === undefined) { - syncTime = lastSyncTimeFromLogs; + const nextSyncTime = await getLastSyncTimeFromLogs(provider); + if (syncTime == null) { + syncTime = nextSyncTime; } LOGGER.info( - `Last registered sync time was: ${new Date(syncTime).toUTCString()}; last detected in logs: ${new Date(lastSyncTimeFromLogs).toUTCString()}`, + `Last registered sync time was: ${new Date(syncTime).toUTCString()}(${syncTime}); last detected in logs:${new Date(nextSyncTime).toUTCString()}(${nextSyncTime})`, ); - expect(lastSyncTimeFromLogs).not.toBeNull(); - expect(lastSyncTimeFromLogs).toBeGreaterThan(syncTime); + expect(nextSyncTime).not.toBeNull(); + expect(nextSyncTime).toBeGreaterThan(syncTime); }).toPass({ intervals: [1_000, 2_000, 10_000], - timeout: syncTime * 2 * 1000, + timeout: synTimeOut * 2 * 1000, }); } @@ -243,6 +258,28 @@ export async function ensureEnvSecretExists( RHSSO76_CLIENT_SECRET: Buffer.from( constants.RHSSO76_CLIENT_SECRET, ).toString("base64"), + + RHBK_DEFAULT_PASSWORD: Buffer.from( + constants.RHSSO76_DEFAULT_PASSWORD, + ).toString("base64"), + RHBK_METADATA_URL: Buffer.from( + `${constants.RHBK_URL}/realms/authProviders`, + ).toString("base64"), + RHBK_CLIENT_ID: Buffer.from(constants.RHBK_CLIENTID).toString("base64"), + RHBK_ADMIN_USERNAME: Buffer.from(constants.RHBK_ADMIN_USERNAME).toString( + "base64", + ), + RHBK_ADMIN_PASSWORD: Buffer.from(constants.RHBK_ADMIN_PASSWORD).toString( + "base64", + ), + RHBK_CALLBACK_URL: Buffer.from( + `${process.env.BASE_URL}/api/auth/oidc/handler/frame`, + ).toString("base64"), + RHBK_CLIENT_SECRET: Buffer.from(constants.RHBK_CLIENT_SECRET).toString( + "base64", + ), + RHBK_URL: Buffer.from(constants.RHBK_URL).toString("base64"), + AUTH_ORG_APP_ID: Buffer.from(constants.AUTH_ORG_APP_ID).toString("base64"), AUTH_ORG_CLIENT_ID: Buffer.from(constants.AUTH_ORG_CLIENT_ID).toString( "base64", @@ -259,6 +296,15 @@ export async function ensureEnvSecretExists( AUTH_PROVIDERS_GH_ORG_NAME: Buffer.from( constants.AUTH_PROVIDERS_GH_ORG_NAME, ).toString("base64"), + GH_USER_PASSWORD: Buffer.from(constants.GH_USER_PASSWORD).toString( + "base64", + ), + AUTH_PROVIDERS_GH_USER_2FA: Buffer.from( + constants.AUTH_PROVIDERS_GH_USER_2FA, + ).toString("base64"), + AUTH_PROVIDERS_GH_ADMIN_2FA: Buffer.from( + constants.AUTH_PROVIDERS_GH_ADMIN_2FA, + ).toString("base64"), }; const secret: V1Secret = { metadata: { @@ -284,3 +330,108 @@ export async function ensureEnvSecretExists( } } } + +export function parseGroupMemberFromEntity(group: GroupEntity) { + if (!group.relations) { + return []; + } + return group.relations + .filter((r) => { + if (r.type == "hasMember") { + return true; + } + }) + .map((r) => r.targetRef.split("/")[1]); +} + +export function parseGroupChildrenFromEntity(group: GroupEntity) { + if (!group.relations) { + return []; + } + return group.relations + .filter((r) => { + if (r.type == "parentOf") { + return true; + } + }) + .map((r) => r.targetRef.split("/")[1]); +} + +export function parseGroupParentFromEntity(group: GroupEntity) { + if (!group.relations) { + return []; + } + return group.relations + .filter((r) => { + if (r.type == "childOf") { + return true; + } + }) + .map((r) => r.targetRef.split("/")[1]); +} + +export async function dumpAllPodsLogs(filePrefix?: string, folder?: string) { + const prefix = filePrefix ? filePrefix : ""; + const folderString = folder ? folder : "/tmp"; + const p = await new KubeClient().coreV1Api.listNamespacedPod( + constants.AUTH_PROVIDERS_NAMESPACE, + undefined, + undefined, + undefined, + undefined, + "app.kubernetes.io/component=backstage", + ); + const pods = p.body.items; + + if (!fs.existsSync(folder)) { + fs.mkdirSync(folder, { recursive: true }); + } + + for (const pod of pods) { + const backstageBackendLogs = + await new KubeClient().coreV1Api.readNamespacedPodLog( + pod.metadata.name, + pod.metadata.namespace, + "backstage-backend", + ); + const dynamicPluginsLogs = + await new KubeClient().coreV1Api.readNamespacedPodLog( + pod.metadata.name, + pod.metadata.namespace, + "install-dynamic-plugins", + ); + fs.writeFileSync( + `${folderString}/${prefix}-backend.txt`, + backstageBackendLogs.body, + { flag: "w" }, + ); + fs.writeFileSync( + `${folderString}/${prefix}-init.txt`, + dynamicPluginsLogs.body, + { flag: "w" }, + ); + } +} + +export async function dumpRHDHUsersAndGroups( + filePrefix?: string, + folder?: string, +) { + const prefix = filePrefix ? filePrefix : ""; + const folderString = folder ? folder : "/tmp"; + const api = new APIHelper(); + api.UseStaticToken(constants.STATIC_API_TOKEN); + const users = await api.getAllCatalogUsersFromAPI(); + const groups = await api.getAllCatalogGroupsFromAPI(); + const locations = await api.getAllCatalogLocationsFromAPI(); + + if (!fs.existsSync(folder)) { + fs.mkdirSync(folder, { recursive: true }); + } + + fs.writeFileSync( + `${folderString}/${prefix}-catalog.txt`, + JSON.stringify({ users, groups, locations }), + { flag: "w" }, + ); +} diff --git a/e2e-tests/playwright/utils/kube-client.ts b/e2e-tests/playwright/utils/kube-client.ts index 6c81de1047..f1523a9d3d 100644 --- a/e2e-tests/playwright/utils/kube-client.ts +++ b/e2e-tests/playwright/utils/kube-client.ts @@ -8,7 +8,6 @@ export class KubeClient { kc: k8s.KubeConfig; constructor() { - LOGGER.info(`Initializing Kubernetes API client`); try { this.kc = new k8s.KubeConfig(); this.kc.loadFromOptions({ diff --git a/e2e-tests/playwright/utils/logger.ts b/e2e-tests/playwright/utils/logger.ts index a6674cb7b4..82dc1e16b5 100644 --- a/e2e-tests/playwright/utils/logger.ts +++ b/e2e-tests/playwright/utils/logger.ts @@ -28,7 +28,7 @@ export const LOGGER = createLogger({ transports: [ new transports.File({ filename: `test-logs.log`, - dirname: process.env.CI ? process.env.ARTIFACTS_DIR : "/tmp", + dirname: process.env.LOGS_FOLDER ? process.env.LOGS_FOLDER : "/tmp", }), ], }); diff --git a/e2e-tests/playwright/utils/ui-helper.ts b/e2e-tests/playwright/utils/ui-helper.ts index bfc84297a8..1773265f7f 100644 --- a/e2e-tests/playwright/utils/ui-helper.ts +++ b/e2e-tests/playwright/utils/ui-helper.ts @@ -436,29 +436,39 @@ export class UIhelper { } async verifyLocationRefreshButtonIsEnabled(locationName: string) { - await this.page.goto("/"); - await this.openSidebar("Catalog"); - await this.selectMuiBox("Kind", "Location"); - await this.verifyHeading("All locations"); - await this.verifyCellsInTable([locationName]); - await this.clickLink(locationName); - await this.verifyHeading(locationName); + await expect(async () => { + await this.page.goto("/"); + await this.openSidebar("Catalog"); + await this.selectMuiBox("Kind", "Location"); + await this.verifyHeading("All locations"); + await this.verifyCellsInTable([locationName]); + await this.clickLink(locationName); + await this.verifyHeading(locationName); + }).toPass({ + intervals: [1_000, 2_000, 5_000], + timeout: 20 * 1000, + }); + + await expect( + this.page.locator(`button[title="Schedule entity refresh"]`), + ).toHaveCount(1); + await this.page.locator(`button[title="Schedule entity refresh"]`).click(); await this.verifyAlertErrorMessage("Refresh scheduled"); const moreButton = await this.page .locator("button[aria-label='more']") .first(); - await moreButton.waitFor({ state: "visible" }); - await moreButton.waitFor({ state: "attached" }); + await moreButton.waitFor({ state: "visible", timeout: 4000 }); + await moreButton.waitFor({ state: "attached", timeout: 4000 }); await moreButton.click(); const unregisterItem = await this.page .locator("li[role='menuitem']") .filter({ hasText: "Unregister entity" }) .first(); - await unregisterItem.waitFor({ state: "visible" }); - await unregisterItem.waitFor({ state: "attached" }); + await unregisterItem.waitFor({ state: "visible", timeout: 4000 }); + await unregisterItem.waitFor({ state: "attached", timeout: 4000 }); expect(unregisterItem).not.toBeDisabled(); } diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000000..cbcc1fbac1 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file