diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b826a30d0..bbb3baf405 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,6 +102,17 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + - name: Save docker image to tar + shell: bash + run: | + docker pull localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }} + docker save -o ${{ runner.temp }}/openam-image.tar localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }} + - name: Upload docker image + uses: actions/upload-artifact@v4 + with: + name: myimage + path: ${{ runner.temp }}/openam-image.tar + - name: Docker test with an external OpenDJ identity store and an embedded OpenDJ configuration store shell: bash run: | @@ -359,3 +370,62 @@ jobs: http://openam3.example.org:8080/openam/json/authenticate | grep tokenId' docker inspect --format="{{json .State.Health.Status}}" test-openam3 | grep -q \"healthy\" + ui-smoke-tests: + runs-on: ubuntu-latest + needs: build-docker + services: + registry: + image: registry:2 + ports: + - 5000:5000 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v6 + + - name: Set env + run: | + export git_version_last="$(curl -i -o - --silent https://api.github.com/repos/OpenIdentityPlatform/OpenAM/releases/latest | grep -m1 "\"name\"" | cut -d\" -f4)" ; echo "last release: $git_version_last" + echo "release_version=$git_version_last" >> $GITHUB_ENV + echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: myimage + path: ${{ runner.temp }} + + - name: Load Docker image + run: | + docker load --input ${{ runner.temp }}/openam-image.tar + docker image ls -a + + - name: Set Integration Test Environment + run: | + echo "127.0.0.1 idp.acme.org sp.mycompany.org" | sudo tee -a /etc/hosts + + - name: Cache Playwright browsers + uses: actions/cache@v5 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-browsers + restore-keys: ${{ runner.os }}-playwright- + + - name: UI Smoke Tests (Playwright) + run: | + cd e2e + npm init -y + npm install @playwright/test + npx playwright install chromium --with-deps + npx playwright test --reporter=list + env: + OPENAM_IMAGE: localhost:5000/${{ env.REPO_LC }}:${{ env.release_version }} + - name: Upload failure artifacts + uses: actions/upload-artifact@v7 + if: ${{ failure() }} + with: + name: failure-ui-java${{ matrix.java }}-${{ matrix.context_label }}-${{ matrix.samples_label }} + retention-days: 1 + path: | + e2e/playwright-report/** + e2e/test-results/** \ No newline at end of file diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000000..970bdbd03b --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,20 @@ +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Copyright 2026 3A Systems, LLC. + + +node_modules/ +package-lock.json +package.json +playwright-report/ +test-results/ \ No newline at end of file diff --git a/e2e/playwright.config.mjs b/e2e/playwright.config.mjs new file mode 100644 index 0000000000..27434a395a --- /dev/null +++ b/e2e/playwright.config.mjs @@ -0,0 +1,31 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ + +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: ".", + testMatch: "**/*.spec.mjs", + timeout: 180000, + retries: 0, + use: { + headless: true, + ignoreHTTPSErrors: true, + screenshot: "only-on-failure", + trace: "retain-on-failure", + }, + reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-report" }]], +}); \ No newline at end of file diff --git a/e2e/saml/bootstrap.sh b/e2e/saml/bootstrap.sh new file mode 100755 index 0000000000..e1620093cb --- /dev/null +++ b/e2e/saml/bootstrap.sh @@ -0,0 +1,237 @@ +#!/bin/bash + +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Copyright 2026 3A Systems, LLC. + + +set -e + +OPENAM_IMAGE=${OPENAM_IMAGE:-"openidentityplatform/openam"} + +echo "Using docker image: ${OPENAM_IMAGE}" + +docker network create openam-saml 2>/dev/null || true + +echo "running OpenAM IDP container..." + +docker run --rm -it -d -h idp.acme.org -p 8080:8080 -p 8000:8000 --network openam-saml --name openam-idp \ + -e JPDA_ADDRESS=*:8000 \ + -e JPDA_TRANSPORT=dt_socket \ + ${OPENAM_IMAGE} catalina.sh jpda run + +echo "waiting for OpenAM IDP to be alive..." + +timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" openam-idp | grep -q \"healthy\"; do sleep 10; done' + + +echo "Running OpenAM IDP setup" + +docker exec -w '/usr/openam/ssoconfiguratortools' openam-idp bash -c \ +'echo "ACCEPT_LICENSES=true +SERVER_URL=http://idp.acme.org:8080 +DEPLOYMENT_URI=/$OPENAM_PATH +BASE_DIR=$OPENAM_DATA_DIR +locale=en_US +PLATFORM_LOCALE=en_US +AM_ENC_KEY= +ADMIN_PWD=passw0rd +AMLDAPUSERPASSWD=p@passw0rd +COOKIE_DOMAIN=idp.acme.org +ACCEPT_LICENSES=true +DATA_STORE=embedded +DIRECTORY_SSL=SIMPLE +DIRECTORY_SERVER=idp.acme.org +DIRECTORY_PORT=50389 +DIRECTORY_ADMIN_PORT=4444 +DIRECTORY_JMX_PORT=1689 +ROOT_SUFFIX=dc=openam,dc=example,dc=org +DS_DIRMGRDN=cn=Directory Manager +DS_DIRMGRPASSWD=passw0rd" > conf.file && java -jar openam-configurator-tool*.jar --file conf.file' + +echo "Setup ssoadm tools for OpenAM IDP" + +docker exec -w '/usr/openam/ssoadmintools' openam-idp bash -c './setup -p /usr/openam/config --acceptLicense' + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-idp bash -c 'echo passw0rd > pwd.txt && chmod 400 pwd.txt' + +echo "Setup COT for OpenAM IDP" +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-idp bash -c \ +'./ssoadm create-cot \ + --adminid amadmin \ + --password-file pwd.txt \ + --realm / \ + --cot MYSAML' + +echo "Create Metadata Template for OpenAM IDP" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-idp bash -c \ +'./ssoadm create-metadata-templ \ + --adminid amadmin \ + --password-file pwd.txt \ + --entityid http://idp.acme.org:8080/openam \ + --identityprovider /idp \ + --idpscertalias test \ + --meta-data-file idp-metadata.xml \ + --extended-data-file idp-extended.xml' + +echo "modify idp-extended.xml" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-idp bash -c ' +sed -i " + //{ + N + s|\s*|\n uid=uid\n| + } +" idp-extended.xml' + +echo "Create hosted identity provider for OpenAM IDP" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-idp bash -c \ +'./ssoadm import-entity \ + --adminid amadmin \ + --password-file pwd.txt \ + --realm / \ + --cot MYSAML \ + --meta-data-file idp-metadata.xml \ + --extended-data-file idp-extended.xml' + +echo "Running OpenAM SP setup" + +docker run --rm -it -d -h sp.mycompany.org -p 8081:8080 -p 8001:8000 --network openam-saml --name openam-sp \ + -e JPDA_ADDRESS=*:8000 \ + -e JPDA_TRANSPORT=dt_socket \ + ${OPENAM_IMAGE} catalina.sh jpda run + +echo "waiting for OpenAM SP to be alive..." + +timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" openam-sp | grep -q \"healthy\"; do sleep 10; done' + +echo "Running OpenAM SP setup" + +docker exec -w '/usr/openam/ssoconfiguratortools' openam-sp bash -c \ +'echo "ACCEPT_LICENSES=true +SERVER_URL=http://sp.mycompany.org:8080 +DEPLOYMENT_URI=/$OPENAM_PATH +BASE_DIR=$OPENAM_DATA_DIR +locale=en_US +PLATFORM_LOCALE=en_US +AM_ENC_KEY= +ADMIN_PWD=passw0rd +AMLDAPUSERPASSWD=p@passw0rd +COOKIE_DOMAIN=sp.mycompany.org +ACCEPT_LICENSES=true +DATA_STORE=embedded +DIRECTORY_SSL=SIMPLE +DIRECTORY_SERVER=sp.mycompany.org +DIRECTORY_PORT=50389 +DIRECTORY_ADMIN_PORT=4444 +DIRECTORY_JMX_PORT=1689 +ROOT_SUFFIX=dc=openam,dc=example,dc=org +DS_DIRMGRDN=cn=Directory Manager +DS_DIRMGRPASSWD=passw0rd" > conf.file && java -jar openam-configurator-tool*.jar --file conf.file' + + +echo "Setup ssoadm tools for OpenAM SP" + +docker exec -w '/usr/openam/ssoadmintools' openam-sp bash -c './setup -p /usr/openam/config --acceptLicense' + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp bash -c 'echo passw0rd > pwd.txt && chmod 400 pwd.txt' + +echo "Setup COT for OpenAM SP" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp bash -c \ +'./ssoadm create-cot \ + --adminid amadmin \ + --password-file pwd.txt \ + --realm / \ + --cot MYSAML' + +echo "Create Metadata Template for OpenAM SP" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp bash -c \ +'./ssoadm create-metadata-templ \ + --adminid amadmin \ + --password-file pwd.txt \ + --entityid http://sp.mycompany.org:8081/openam \ + --serviceprovider /sp \ + --spscertalias test \ + --meta-data-file sp-metadata.xml \ + --extended-data-file sp-extended.xml' + +echo "modify sp-metadata.xml" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp bash -c \ + "sed -i 's/http:\/\/sp.mycompany.org:8080/http:\/\/sp.mycompany.org:8081/g' sp-metadata.xml" + +echo "modify sp-extended.xml" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp bash -c ' +sed -i " + //{ + N + s|\s*|\n \*=\*\n| + } +" sp-extended.xml' + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp \ + sed -i '/name="autofedEnabled"/{n;s/false/true/}' sp-extended.xml + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp \ + sed -i '/name="autofedAttribute"/{n;s||uid|}' sp-extended.xml + + +echo "Create hosted identity provider for OpenAM IDP" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp bash -c \ +'./ssoadm import-entity \ + --adminid amadmin \ + --password-file pwd.txt \ + --realm / \ + --cot MYSAML \ + --meta-data-file sp-metadata.xml \ + --extended-data-file sp-extended.xml' + +echo "Exchange providers metadata between contaners" + +docker exec openam-sp cat /usr/openam/ssoadmintools/openam/bin/sp-metadata.xml | docker exec -i openam-idp bash -c 'cat > /usr/openam/ssoadmintools/openam/bin/remote-sp-metadata.xml' + +docker exec openam-idp cat /usr/openam/ssoadmintools/openam/bin/idp-metadata.xml | docker exec -i openam-sp bash -c 'cat > /usr/openam/ssoadmintools/openam/bin/remote-idp-metadata.xml' + +echo "Create remote SP in OpenAM IDP" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-idp bash -c \ +'./ssoadm import-entity \ + --adminid amadmin \ + --password-file pwd.txt \ + --realm / \ + --cot MYSAML \ + --meta-data-file remote-sp-metadata.xml' + +echo "Create remote IDP in OpenAM SP" + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp bash -c \ +'./ssoadm import-entity \ + --adminid amadmin \ + --password-file pwd.txt \ + --realm / \ + --cot MYSAML \ + --meta-data-file remote-idp-metadata.xml' + +docker exec -w '/usr/openam/ssoadmintools/openam/bin' openam-sp bash -c \ +'./ssoadm set-realm-svc-attrs \ + --adminid amadmin \ + --password-file pwd.txt \ + --realm / \ + --servicename iPlanetAMAuthService \ + --attributevalues iplanet-am-auth-dynamic-profile-creation=ignore' \ No newline at end of file diff --git a/e2e/saml/saml-test.spec.mjs b/e2e/saml/saml-test.spec.mjs new file mode 100644 index 0000000000..82af561588 --- /dev/null +++ b/e2e/saml/saml-test.spec.mjs @@ -0,0 +1,127 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ + + +// openam.spec.mjs – ESM edition +import { test, expect } from "@playwright/test"; +import { execSync } from "child_process"; +import { resolve } from "path"; +import { fileURLToPath } from "url"; + +/** + * OpenAM XUI Login Test Suite + * + * Configuration (override via environment variables): + * OPENAM_USERNAME – login username (default: demo) + * OPENAM_PASSWORD – login password (default: changeit) + * BOOTSTRAP_SCRIPT – path to the startup script (default: ./bootstrap.sh) + * SHUTDOWN_SCRIPT – path to the shutdown script (default: ./shutdown.sh) + */ + +// ─── __dirname equivalent in ESM ────────────────────────────────────────────── +const __filename = fileURLToPath(import.meta.url); +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +// ─── Configuration ──────────────────────────────────────────────────────────── +const USERNAME = process.env.OPENAM_USERNAME ?? "demo"; +const PASSWORD = process.env.OPENAM_PASSWORD ?? "changeit"; +const BOOTSTRAP_SCRIPT = process.env.BOOTSTRAP_SCRIPT ?? "./bootstrap.sh"; +const SHUTDOWN_SCRIPT = process.env.SHUTDOWN_SCRIPT ?? "./shutdown.sh"; + +// Derived URLs +const LOGIN_URL = "http://sp.mycompany.org:8081/openam/spssoinit?metaAlias=/sp&idpEntityID=http%3A//idp.acme.org%3A8080/openam&RelayState=http%3A//sp.mycompany.org%3A8081/openam"; +const EXPECTED_IDP_URL_PATTERN = /idp\.acme\.org/; +const EXPECTED_SP_URL_PATTERN = /sp\.mycompany\.org/; + +// ─── Selectors (XUI / LESS-based OpenAM UI) ─────────────────────────────────── +const SEL = { + usernameInput: "#idToken1", // + passwordInput: "#idToken2", // + loginButton: 'input[type="submit"]', // + userIdElement: "#input-username", +}; + +const execScript = (scriptPath) => { + try { + execSync(`bash "${scriptPath}"`, { + encoding: "utf-8", + timeout: 300_000, // 5 minutes max + stdio: "inherit", + }); + } catch (err) { + throw new Error( + `${scriptPath} exited with code ${err.status}: ${err.message}` + ); + } +} + +// ─── Bootstrap – run once before all tests ──────────────────────────────────── +test.beforeAll(() => { + const scriptPath = resolve(__dirname, BOOTSTRAP_SCRIPT); + console.log(`\n▶ Running bootstrap script: ${scriptPath}`); + execScript(scriptPath); + +}); + +// ─── Shutdown – run once after all tests ──────────────────────────────────── +test.afterAll(() => { + const scriptPath = resolve(__dirname, SHUTDOWN_SCRIPT); + console.log(`\n▶ Running shutdown script: ${scriptPath}`); + execScript(scriptPath); +}); + +// ─── Tests ──────────────────────────────────────────────────────────────────── +test.describe("OpenAM XUI - Login flow", () => { + test("should log in as demo and reach the authenticated page", async ({ page }) => { + // ── 1. Open the login page ────────────────────────────────────────────── + console.log(`Navigating to: ${LOGIN_URL}`); + await page.goto(LOGIN_URL); + + await page.waitForURL(EXPECTED_IDP_URL_PATTERN, { + timeout: 20_000, + waitUntil: "networkidle", + }); + + await expect( + page.locator(SEL.usernameInput), + "Username input should be visible" + ).toBeVisible({ timeout: 15_000 }); + + // ── 2. Enter credentials ──────────────────────────────────────────────── + await page.fill(SEL.usernameInput, USERNAME); + await page.fill(SEL.passwordInput, PASSWORD); + + // ── 3. Click Login ────────────────────────────────────────────────────── + await page.click(SEL.loginButton); + + // ── 4. Wait for the post-login redirect ───────────────────────────────── + await page.waitForURL(EXPECTED_SP_URL_PATTERN, { + timeout: 20_000, + waitUntil: "networkidle", + }); + + // ── 5. Assert target URL ───────────────────────────────────────────────── + const currentUrl = page.url(); + console.log(`Redirected to: ${currentUrl}`); + expect(currentUrl, "URL should match the post-login pattern").toMatch(EXPECTED_SP_URL_PATTERN); + + // ── 6. Assert user-id element is present ──────────────────────────────── + await expect( + page.locator(SEL.userIdElement).first(), + "Authenticated user-id element should be visible after login" + ).toBeVisible({ timeout: 10_000 }); + }); +}); diff --git a/e2e/saml/shutdown.sh b/e2e/saml/shutdown.sh new file mode 100755 index 0000000000..685f76cb37 --- /dev/null +++ b/e2e/saml/shutdown.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Copyright 2026 3A Systems, LLC. + + +echo "Stopping docker containers..." +docker stop openam-idp openam-sp + +echo "Removing network..." +docker network rm openam-saml +echo "Finished" \ No newline at end of file diff --git a/openam-core/src/main/java/com/sun/identity/idm/plugins/internal/SpecialRepo.java b/openam-core/src/main/java/com/sun/identity/idm/plugins/internal/SpecialRepo.java index dbc46e140b..2acd23555a 100644 --- a/openam-core/src/main/java/com/sun/identity/idm/plugins/internal/SpecialRepo.java +++ b/openam-core/src/main/java/com/sun/identity/idm/plugins/internal/SpecialRepo.java @@ -566,7 +566,7 @@ public RepoSearchResults search(SSOToken token, IdType type, CrestQuery crestQue if (uidVals != null && !uidVals.isEmpty()) { pattern = (String) uidVals.iterator().next(); if (crestQuery.isEscapeQueryId()) { - pattern = crestQuery.getEscapedQueryId(); + pattern = Filter.escapeAssertionValue(pattern); } } else { // pattern is "*" and avPairs is not empty, so return