Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
850fc93
test(playwright): add nightly SSO login spec starting with Okta
Apr 8, 2026
45d8c95
ci: add dedicated SSO Login Nightly workflow
Apr 8, 2026
ae7f190
refactor(playwright): simplify ssoAuth helpers
Apr 8, 2026
3f9fbcc
refactor(playwright): address SSO suite review feedback
Apr 8, 2026
8b3f993
ci: avoid CodeQL "Excessive Secrets Exposure" in SSO Login Nightly
Apr 8, 2026
13849af
refactor(playwright): capture/restore real security config in SSO suite
Apr 9, 2026
3143312
ci(sso-login): extract per-provider composite action
Apr 9, 2026
8a5706a
refactor(playwright): move Okta tenant config to a repo constant
Apr 9, 2026
325405e
ci(sso-login): move timeout-minutes from composite step to job level
Apr 9, 2026
31a3cf2
ci(sso-login): consolidate dispatcher + composite action into one file
Apr 9, 2026
e0c3f4f
ci(sso-login): drop provider-prefix bash step; use case-insensitive l…
Apr 9, 2026
a37aff0
ci(sso-login): drop redundant case-insensitivity comment
Apr 9, 2026
b054b6f
ci(sso-login): pin playwright install to 1.57.0 to match package.json
Apr 9, 2026
e0a148e
Merge branch 'main' into test/playwright/sso-login-okta
siddhant1 Apr 9, 2026
891d976
refactor(playwright): address SSO suite review comments [skip ci]
Apr 9, 2026
39fb53a
saml
Apr 10, 2026
633d185
test(playwright): add SAML providers to SSO login nightly
Apr 14, 2026
b12e7af
refactor(playwright): fetch Azure SAML IdP cert at runtime
Apr 14, 2026
484105b
refactor(playwright): drop hosted Azure SAML provider
Apr 14, 2026
01406f3
test(playwright): cover SSO session lifecycle end-to-end
Apr 14, 2026
824aa35
Merge branch 'main' into test/playwright/sso-login-okta
siddhant1 Apr 15, 2026
6824868
remove slow from individual spec
siddhant1 Apr 15, 2026
fc1655d
remove slow from beforeAll
siddhant1 Apr 15, 2026
12d913c
style(playwright): fix SSOLogin spec prettier issues
Apr 15, 2026
b7fa14a
Merge branch 'main' into test/playwright/sso-login-okta
siddhant1 Apr 15, 2026
be3d298
test(playwright): tighten SSO sign-in locator and await logout response
Apr 15, 2026
49394db
Merge branch 'main' into test/playwright/sso-login-okta
siddhant1 Apr 15, 2026
eb1dc7b
fix
Apr 15, 2026
2f382c3
Merge branch 'test/playwright/sso-login-okta' of https://github.com/o…
Apr 15, 2026
014eeae
Update openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/SSOL…
siddhant1 Apr 15, 2026
7d64bfc
fix
siddhant1 Apr 15, 2026
b6032a8
Merge branch 'main' into test/playwright/sso-login-okta
siddhant1 Apr 15, 2026
2104639
test(playwright): resolve SSO creds via env vars, drop keycloak-googl…
Apr 15, 2026
f918b65
test(playwright): post SSO login nightly results to Slack
Apr 15, 2026
5106511
Merge branch 'main' into test/playwright/sso-login-okta
siddhant1 Apr 15, 2026
c13e1dc
fix(playwright): drop logout response wait in SSO spec
Apr 15, 2026
2130e87
Merge branch 'main' into test/playwright/sso-login-okta
siddhant1 Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .github/workflows/playwright-sso-login-nightly.yml
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
Comment thread
siddhant1 marked this conversation as resolved.
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
Comment thread
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 warning

Code 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)]
Comment thread
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 \
Comment thread
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
9 changes: 9 additions & 0 deletions openmetadata-ui/src/main/resources/ui/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,21 @@ export default defineConfig({
teardown: 'SearchRBAC',
testIgnore: [
'**/nightly/**',
'**/Auth/**',
'**/DataAssetRulesEnabled.spec.ts',
'**/DataAssetRulesDisabled.spec.ts',
'**/SystemCertificationTags.spec.ts',
'**/SearchRBAC.spec.ts',
],
},
{
name: 'sso-auth',
testMatch: '**/Auth/**/*.spec.ts',
use: { ...devices['Desktop Chrome'] },
// Provider swaps mutate global server state — keep tests strictly serial.
fullyParallel: false,
Comment thread
siddhant1 marked this conversation as resolved.
workers: 1,
},
Comment thread
siddhant1 marked this conversation as resolved.
{
name: 'SearchRBAC',
testMatch: '**/SearchRBAC.spec.ts',
Expand Down
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;
Comment thread
siddhant1 marked this conversation as resolved.
Outdated
Comment thread
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';
Comment thread
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()
);
Comment thread
siddhant1 marked this conversation as resolved.
} finally {
await afterAction();
}
Comment thread
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);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selector for the SSO button is very broad (.signin-button) and could match non-button elements or multiple nodes. Other Auth specs in this repo use button.signin-button for this control. Tightening the locator here would reduce flakiness and align with existing patterns.

Copilot uses AI. Check for mistakes.
});

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 }
);
Comment thread
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();
Comment thread
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`
);
}
};
Loading
Loading