-
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 5 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,148 @@ | ||
| # 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. | ||
|
|
||
| # Nightly end-to-end SSO login tests. | ||
| # | ||
| # Purpose: | ||
| # Drive a real login round-trip through an external identity provider | ||
| # (Okta in Phase 1) and validate the resulting OpenMetadata session. | ||
| # Complements playwright-sso-tests.yml, which only exercises the SSO | ||
| # configuration form UI and does not perform an actual login. | ||
| # | ||
| # Test location: | ||
| # openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/SSOLogin.spec.ts | ||
| # | ||
| # Triggers: | ||
| # - Nightly cron at 03:00 UTC | ||
| # - Manual workflow_dispatch with provider selection | ||
|
|
||
| name: SSO Login Nightly | ||
|
|
||
| on: | ||
| schedule: | ||
| # Every night at 03:00 UTC | ||
| - cron: '0 3 * * *' | ||
| workflow_dispatch: | ||
| inputs: | ||
| sso_provider: | ||
| description: "SSO provider to test" | ||
| required: true | ||
| default: "okta" | ||
| type: choice | ||
| options: | ||
| - okta | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: sso-login-nightly-${{ github.event.inputs.sso_provider || 'scheduled' }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| sso-login: | ||
| runs-on: ubuntu-latest | ||
| environment: test | ||
|
|
||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| provider: | ||
| ${{ github.event_name == 'schedule' | ||
| && fromJSON('["okta"]') | ||
| || github.event.inputs.sso_provider | ||
| && fromJSON(format('["{0}"]', github.event.inputs.sso_provider)) | ||
| || fromJSON('["okta"]') }} | ||
|
|
||
| steps: | ||
| - name: Free Disk Space (Ubuntu) | ||
| uses: jlumbroso/free-disk-space@main | ||
| 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" | ||
| # Ingestion is not required for SSO login tests. | ||
| 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: Run SSO Login Spec | ||
| working-directory: openmetadata-ui/src/main/resources/ui | ||
| run: | | ||
| npx playwright test playwright/e2e/Auth/SSOLogin.spec.ts \ | ||
| --project=sso-auth \ | ||
|
siddhant1 marked this conversation as resolved.
|
||
| --workers=1 | ||
| env: | ||
| SSO_PROVIDER_TYPE: ${{ matrix.provider }} | ||
| # Reference each provider's secrets statically so GitHub Actions | ||
| # exposes only the credentials this step needs — not the entire | ||
| # org/repo secret store. A `secrets[format(...)]` style lookup | ||
| # would defeat static analysis and trigger CodeQL's | ||
| # "Excessive Secrets Exposure" rule. When a new provider lands, | ||
| # add a sibling step gated by `if: matrix.provider == '<provider>'` | ||
| # with that provider's static secret references. | ||
| SSO_USERNAME: ${{ secrets.OKTA_SSO_USERNAME }} | ||
| SSO_PASSWORD: ${{ secrets.OKTA_SSO_PASSWORD }} | ||
| # Okta-specific overrides (all optional — defaults are baked into | ||
| # playwright/utils/sso-providers/okta.ts and point at the | ||
| # integrator-9351624.okta.com dev tenant). | ||
| OKTA_SSO_CLIENT_ID: ${{ secrets.OKTA_SSO_CLIENT_ID }} | ||
| OKTA_SSO_DOMAIN: ${{ secrets.OKTA_SSO_DOMAIN }} | ||
| OKTA_SSO_PRINCIPAL_DOMAIN: ${{ secrets.OKTA_SSO_PRINCIPAL_DOMAIN }} | ||
| PLAYWRIGHT_IS_OSS: true | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| timeout-minutes: 30 | ||
|
|
||
| - 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,49 @@ | ||
| /* | ||
| * 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 { SSOConfig } from '../utils/sso'; | ||
|
|
||
| // 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', | ||
| OKTA_CLIENT_ID: 'OKTA_SSO_CLIENT_ID', | ||
| OKTA_DOMAIN: 'OKTA_SSO_DOMAIN', | ||
| OKTA_PRINCIPAL_DOMAIN: 'OKTA_SSO_PRINCIPAL_DOMAIN', | ||
| } as const; | ||
|
|
||
| export const BASIC_AUTH_CONFIG: SSOConfig = { | ||
| authenticationConfiguration: { | ||
| provider: 'basic', | ||
| providerName: 'basic', | ||
| authority: '', | ||
| clientId: '', | ||
| callbackUrl: '', | ||
| publicKeyUrls: [`${OM_BASE_URL}/api/v1/system/config/jwks`], | ||
| jwtPrincipalClaims: ['email', 'preferred_username', 'sub'], | ||
| enableSelfSignup: true, | ||
| }, | ||
| authorizerConfiguration: { | ||
| className: 'org.openmetadata.service.security.DefaultAuthorizer', | ||
| containerRequestFilter: 'org.openmetadata.service.security.JwtFilter', | ||
| adminPrincipals: ['admin'], | ||
| principalDomain: 'open-metadata.org', | ||
| enforcePrincipalDomain: false, | ||
| enableSecureSocketConnection: false, | ||
| }, | ||
| }; | ||
|
siddhant1 marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| /* | ||
| * 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, | ||
| restoreBasicAuth, | ||
| 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; | ||
|
|
||
| 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); | ||
| await applyProviderConfig(apiContext, helper.buildConfigPayload()); | ||
| } finally { | ||
| await afterAction(); | ||
| } | ||
|
siddhant1 marked this conversation as resolved.
|
||
| } | ||
| ); | ||
|
|
||
| test.afterAll('Restore basic auth configuration', async () => { | ||
| if (!adminJwt) { | ||
| return; | ||
| } | ||
|
|
||
| const adminContext = await getAuthContext(adminJwt); | ||
|
|
||
| try { | ||
| await restoreBasicAuth(adminContext); | ||
| } 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 } | ||
| ); | ||
|
|
||
| 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(); | ||
| } | ||
| ); | ||
|
|
||
| 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,38 @@ | ||
| /* | ||
| * 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 { SSOConfig } from '../sso'; | ||
| import { ProviderCredentials } from '../ssoAuth'; | ||
| import { oktaProviderHelper } from './okta'; | ||
|
|
||
| export interface ProviderHelper { | ||
| expectedButtonText: string; | ||
| loginUrlPattern: RegExp; | ||
| buildConfigPayload: () => SSOConfig; | ||
| 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.