-
Notifications
You must be signed in to change notification settings - Fork 2.1k
test(playwright): add nightly SSO login spec starting #27164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
850fc93
45d8c95
ae7f190
3f9fbcc
8b3f993
13849af
3143312
8a5706a
325405e
31a3cf2
e0c3f4f
a37aff0
b054b6f
e0a148e
891d976
39fb53a
633d185
b12e7af
484105b
01406f3
824aa35
6824868
fc1655d
12d913c
b7fa14a
be3d298
49394db
eb1dc7b
2f382c3
014eeae
7d64bfc
b6032a8
2104639
f918b65
5106511
c13e1dc
2130e87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # Copyright 2025 Collate | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| name: SSO Login Nightly | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: '0 3 * * *' | ||
| workflow_dispatch: | ||
| inputs: | ||
| sso_provider: | ||
| description: 'SSO provider (or "all")' | ||
| required: true | ||
| default: okta | ||
| type: choice | ||
| options: | ||
| - okta | ||
| - all | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: sso-login-nightly-${{ github.event.inputs.sso_provider || 'scheduled' }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| # To onboard a new provider: | ||
| # 1. Add its lowercase name to the matrix array below AND to the | ||
| # dispatch `options:` list above. | ||
| # 2. Add <PROVIDER>_SSO_USERNAME (variable) and <PROVIDER>_SSO_PASSWORD | ||
| # (secret) to the `test` environment. | ||
| # 3. Register the helper in playwright/utils/sso-providers/index.ts. | ||
| sso-login: | ||
| runs-on: ubuntu-latest | ||
| environment: test | ||
| timeout-minutes: 45 | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| provider: | ||
| ${{ (github.event_name == 'schedule' || github.event.inputs.sso_provider == 'all') | ||
| && fromJSON('["okta"]') | ||
| || fromJSON(format('["{0}"]', github.event.inputs.sso_provider)) }} | ||
| steps: | ||
| - name: Free Disk Space (Ubuntu) | ||
| uses: jlumbroso/free-disk-space@main | ||
|
siddhant1 marked this conversation as resolved.
|
||
| with: | ||
| tool-cache: false | ||
| android: true | ||
| dotnet: true | ||
| haskell: true | ||
| large-packages: false | ||
| swap-storage: true | ||
| docker-images: false | ||
|
|
||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Cache Maven Dependencies | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: ~/.m2 | ||
| key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} | ||
| restore-keys: | | ||
| ${{ runner.os }}-maven- | ||
|
|
||
| - name: Setup OpenMetadata Test Environment | ||
| uses: ./.github/actions/setup-openmetadata-test-environment | ||
| with: | ||
| python-version: "3.10" | ||
| args: "-d postgresql -i false" | ||
| ingestion_dependency: "all" | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version-file: 'openmetadata-ui/src/main/resources/ui/.nvmrc' | ||
|
|
||
| - name: Install dependencies | ||
| working-directory: openmetadata-ui/src/main/resources/ui/ | ||
| run: yarn --ignore-scripts --frozen-lockfile | ||
|
|
||
| - name: Install Playwright Browsers | ||
| run: npx playwright@1.51.1 install chromium --with-deps | ||
|
|
||
| - name: Compute provider prefix | ||
| run: echo "PROVIDER_PREFIX=$(echo '${{ matrix.provider }}' | tr '[:lower:]' '[:upper:]')" >> "$GITHUB_ENV" | ||
|
|
||
| - name: Run SSO Login Spec | ||
| working-directory: openmetadata-ui/src/main/resources/ui | ||
| env: | ||
| SSO_PROVIDER_TYPE: ${{ matrix.provider }} | ||
| SSO_USERNAME: ${{ vars[format('{0}_SSO_USERNAME', env.PROVIDER_PREFIX)] }} | ||
| SSO_PASSWORD: ${{ secrets[format('{0}_SSO_PASSWORD', env.PROVIDER_PREFIX)] }} | ||
Check warningCode scanning / CodeQL Excessive Secrets Exposure Medium
All organization and repository secrets are passed to the workflow runner in
secrets[format('{0}_SSO_PASSWORD', env.PROVIDER_PREFIX)] Error loading related location Loading |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| PLAYWRIGHT_IS_OSS: true | ||
| run: | | ||
| npx playwright test playwright/e2e/Auth/SSOLogin.spec.ts \ | ||
| --project=sso-auth \ | ||
|
siddhant1 marked this conversation as resolved.
|
||
| --workers=1 | ||
|
|
||
| - name: Upload HTML report | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: sso-login-html-report-${{ matrix.provider }} | ||
| path: openmetadata-ui/src/main/resources/ui/playwright/output/playwright-report | ||
| retention-days: 5 | ||
|
|
||
| - name: Clean Up | ||
| if: always() | ||
| run: | | ||
| cd ./docker/development | ||
| docker compose down --remove-orphans | ||
| sudo rm -rf ${PWD}/docker-volume | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /* | ||
| * Copyright 2025 Collate. | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| // Mirrors playwright.config.ts `baseURL` resolution so the SSO config we | ||
| // PUT into OpenMetadata always matches the host the Playwright suite is | ||
| // driving. Defaults to the local stack. | ||
| export const OM_BASE_URL = | ||
| process.env.PLAYWRIGHT_TEST_BASE_URL ?? 'http://localhost:8585'; | ||
|
|
||
| export const SSO_ENV = { | ||
| PROVIDER_TYPE: 'SSO_PROVIDER_TYPE', | ||
| USERNAME: 'SSO_USERNAME', | ||
| PASSWORD: 'SSO_PASSWORD', | ||
| } as const; | ||
|
siddhant1 marked this conversation as resolved.
Outdated
siddhant1 marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| /* | ||
| * Copyright 2025 Collate. | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { SSO_ENV } from '../../constant/ssoAuth'; | ||
| import { performAdminLogin } from '../../utils/admin'; | ||
| import { getAuthContext } from '../../utils/common'; | ||
| import { getProviderHelper, ProviderHelper } from '../../utils/sso-providers'; | ||
| import { | ||
| applyProviderConfig, | ||
| fetchSecurityConfig, | ||
| restoreSecurityConfig, | ||
| SecurityConfigSnapshot, | ||
| verifyLoggedInUserMatches, | ||
| } from '../../utils/ssoAuth'; | ||
| import { getToken } from '../../utils/tokenStorage'; | ||
|
siddhant1 marked this conversation as resolved.
|
||
|
|
||
| const providerType = process.env[SSO_ENV.PROVIDER_TYPE] ?? ''; | ||
| const username = process.env[SSO_ENV.USERNAME] ?? ''; | ||
| const password = process.env[SSO_ENV.PASSWORD] ?? ''; | ||
|
|
||
| test.describe('SSO Login', { tag: ['@sso', '@Platform'] }, () => { | ||
| // eslint-disable-next-line playwright/no-skipped-test -- conditional skip on required env vars; the suite only runs when SSO credentials are provided by CI or the developer | ||
| test.skip( | ||
| !providerType || !username || !password, | ||
| `${SSO_ENV.PROVIDER_TYPE}, ${SSO_ENV.USERNAME}, and ${SSO_ENV.PASSWORD} env vars must be set` | ||
| ); | ||
|
|
||
| test.describe.configure({ mode: 'serial' }); | ||
|
|
||
| let helper: ProviderHelper; | ||
| let adminJwt: string | undefined; | ||
| let originalSecurityConfig: SecurityConfigSnapshot | undefined; | ||
|
|
||
| test.beforeAll( | ||
| 'Swap OpenMetadata server to target SSO provider', | ||
| async ({ browser }) => { | ||
| test.slow(); | ||
| helper = getProviderHelper(providerType); | ||
| const { apiContext, afterAction, page } = await performAdminLogin( | ||
| browser | ||
| ); | ||
|
|
||
| try { | ||
| adminJwt = await getToken(page); | ||
|
|
||
| if (!adminJwt) { | ||
| throw new Error( | ||
| 'Failed to capture admin JWT before SSO swap — aborting to avoid leaving server in SSO mode' | ||
| ); | ||
| } | ||
|
|
||
| originalSecurityConfig = await fetchSecurityConfig(apiContext); | ||
| await applyProviderConfig( | ||
| apiContext, | ||
| originalSecurityConfig, | ||
| helper.buildConfigPayload() | ||
| ); | ||
|
siddhant1 marked this conversation as resolved.
|
||
| } finally { | ||
| await afterAction(); | ||
| } | ||
|
siddhant1 marked this conversation as resolved.
|
||
| } | ||
| ); | ||
|
|
||
| test.afterAll('Restore original security configuration', async () => { | ||
| if (!adminJwt || !originalSecurityConfig) { | ||
| return; | ||
| } | ||
|
|
||
| const adminContext = await getAuthContext(adminJwt); | ||
|
|
||
| try { | ||
| await restoreSecurityConfig(adminContext, originalSecurityConfig); | ||
| } finally { | ||
| await adminContext.dispose(); | ||
| } | ||
| }); | ||
|
|
||
| test('should display SSO sign-in button on /signin', async ({ page }) => { | ||
| await page.goto('/signin'); | ||
|
|
||
| await expect(page.getByTestId('login-form-container')).toBeVisible(); | ||
|
|
||
| const signInButton = page.locator('.signin-button'); | ||
|
|
||
| await expect(signInButton).toBeVisible(); | ||
| await expect(signInButton).toContainText(helper.expectedButtonText); | ||
| await expect(page.getByTestId('email')).toHaveCount(0); | ||
|
||
| }); | ||
|
|
||
| test('should complete full SSO login and verify user session', async ({ | ||
| page, | ||
| }) => { | ||
| test.slow(); | ||
|
|
||
| await test.step('Click SSO button and redirect to IdP', async () => { | ||
| await page.goto('/signin'); | ||
|
|
||
| const signInButton = page.locator('.signin-button'); | ||
|
|
||
| await expect(signInButton).toBeVisible(); | ||
| await signInButton.click(); | ||
| await page.waitForURL(helper.loginUrlPattern, { timeout: 45_000 }); | ||
| }); | ||
|
|
||
| await test.step('Authenticate at the identity provider', async () => { | ||
| await helper.performProviderLogin(page, { username, password }); | ||
| }); | ||
|
|
||
| await test.step('Return to OpenMetadata and complete self-signup if needed', async () => { | ||
| await page.waitForURL( | ||
| (url) => | ||
| url.pathname.endsWith('/signup') || url.pathname.endsWith('/my-data'), | ||
| { timeout: 60_000 } | ||
| ); | ||
|
siddhant1 marked this conversation as resolved.
|
||
|
|
||
| if (page.url().includes('/signup')) { | ||
| const createButton = page.getByRole('button', { name: /create/i }); | ||
|
|
||
| await expect(createButton).toBeEnabled(); | ||
| await createButton.click(); | ||
| await page.waitForURL('**/my-data', { timeout: 60_000 }); | ||
| } | ||
|
|
||
| await expect(page.getByTestId('dropdown-profile')).toBeVisible(); | ||
|
siddhant1 marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
|
||
| await test.step('Verify JWT against loggedInUser API', async () => { | ||
| await verifyLoggedInUserMatches(page, username); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /* | ||
| * Copyright 2025 Collate. | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| import { Page } from '@playwright/test'; | ||
| import { ProviderConfigOverride, ProviderCredentials } from '../ssoAuth'; | ||
| import { oktaProviderHelper } from './okta'; | ||
|
|
||
| export interface ProviderHelper { | ||
| expectedButtonText: string; | ||
| loginUrlPattern: RegExp; | ||
| buildConfigPayload: () => ProviderConfigOverride; | ||
| performProviderLogin: ( | ||
| page: Page, | ||
| credentials: ProviderCredentials | ||
| ) => Promise<void>; | ||
| } | ||
|
|
||
| export const getProviderHelper = (providerType: string): ProviderHelper => { | ||
| switch (providerType) { | ||
| case 'okta': | ||
| return oktaProviderHelper; | ||
| default: | ||
| throw new Error( | ||
| `No SSO provider helper registered for "${providerType}". ` + | ||
| `Supported providers: okta` | ||
| ); | ||
| } | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.