diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0aa7a40ed9e..88be4166929 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ on: push: branches: [main, staging, dev] pull_request: - branches: [main, staging, dev] + branches: [main, staging, dev, e2e/settings-playwright] # Docs content and markdown don't affect the app build or images; push # runs stay unfiltered because they feed the deploy pipeline. paths-ignore: diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index ed348a41446..16168dc5990 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -258,4 +258,104 @@ jobs: AWS_REGION: 'us-west-2' ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only TURBO_CACHE_DIR: .turbo - run: bunx turbo run build --filter=sim \ No newline at end of file + run: bunx turbo run build --filter=sim + + settings-e2e: + name: Settings E2E (informational) + runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 45 + continue-on-error: true + + services: + postgres: + image: pgvector/pgvector:pg17 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.13 + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + + - name: Mount Bun cache (Sticky Disk) + uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1 + with: + key: ${{ github.repository }}-bun-cache-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }} + path: ~/.bun/install/cache + + - name: Mount node_modules (Sticky Disk) + uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1 + with: + key: ${{ github.repository }}-node-modules-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }} + path: ./node_modules + + - name: Mount Playwright browsers (Sticky Disk) + uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1 + with: + key: ${{ github.repository }}-playwright-browsers-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }} + path: ~/.cache/ms-playwright + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Configure E2E hostnames + run: echo "127.0.0.1 e2e.sim.ai mcp.e2e.sim.ai" | sudo tee -a /etc/hosts + + - name: Install Chromium + working-directory: apps/sim + run: bun run test:e2e:install-browsers -- --with-deps + + - name: Run settings E2E foundation + timeout-minutes: 40 + working-directory: apps/sim + env: + CI: 'true' + E2E_PG_ADMIN_URL: 'postgresql://postgres:postgres@127.0.0.1:5432/postgres' + run: bun run test:e2e + + - name: Check E2E diagnostics eligibility + id: e2e_diagnostics + if: failure() + shell: bash + run: | + shopt -s nullglob + markers=(apps/sim/e2e/.runs/*/markers/leak-scan-complete.json) + if (( ${#markers[@]} > 0 )); then + echo "safe=true" >> "$GITHUB_OUTPUT" + else + echo "safe=false" >> "$GITHUB_OUTPUT" + fi + + - name: Upload E2E diagnostics + if: failure() && steps.e2e_diagnostics.outputs.safe == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: settings-e2e-${{ github.run_id }} + path: | + apps/sim/playwright-report/ + apps/sim/test-results/ + apps/sim/e2e/.runs/ + !apps/sim/e2e/.runs/**/auth/** + !apps/sim/e2e/.runs/**/private/** + !apps/sim/e2e/.runs/**/homes/** + if-no-files-found: ignore + include-hidden-files: true + retention-days: 7 \ No newline at end of file diff --git a/.gitignore b/.gitignore index a8a0d8e2fde..109495320d0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,11 @@ package-lock.json # testing /coverage /apps/**/coverage +/apps/**/playwright-report/ +/apps/**/test-results/ +/apps/**/e2e/.runs/ +/apps/**/e2e/.auth/ +/apps/**/e2e/.cache/ # next.js /.next/ @@ -31,6 +36,7 @@ package-lock.json **/dist/ **/standalone/ sim-standalone.tar.gz +/.artifacts/ # redis dump.rdb diff --git a/apps/docs/content/docs/en/platform/enterprise/sso.mdx b/apps/docs/content/docs/en/platform/enterprise/sso.mdx index ee3d53be3ec..112ec7f28ef 100644 --- a/apps/docs/content/docs/en/platform/enterprise/sso.mdx +++ b/apps/docs/content/docs/en/platform/enterprise/sso.mdx @@ -252,7 +252,7 @@ SSO provisioning creates internal organization members. External workspace membe }, { question: "A user already has an account with the same email — what happens when they sign in with SSO?", - answer: "Sim links the SSO identity to the existing account automatically, as long as your identity provider reports the email as verified (email_verified) or the provider is trusted. Most OIDC providers (Okta, Google Workspace, Auth0) assert email_verified, so linking just works. If sign-in fails with 'account not linked' — common with SAML providers that omit the claim — add the provider's ID to SSO_TRUSTED_PROVIDER_IDS on self-hosted and restart." + answer: "Before verified-domain enforcement is activated, Sim links the SSO identity when your identity provider reports a verified email or the provider is operator-trusted. After enforcement is activated, the provider's verified organization domain is the account-linking trust boundary." }, { question: "Who can configure SSO on Sim Cloud?", @@ -281,11 +281,16 @@ Self-hosted deployments use environment variables instead of the billing/plan ch SSO_ENABLED=true NEXT_PUBLIC_SSO_ENABLED=true +# Enable only after the SSO schema migration, provider audit, and approved +# domain-verification backfill described below. +SSO_DOMAIN_VERIFICATION_ENABLED=false + # Required if you want users auto-added to your organization on first SSO sign-in ORGANIZATIONS_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true -# Optional: comma-separated SSO provider IDs to trust for automatic account linking +# Optional while domain verification is disabled: comma-separated SSO provider IDs +# to trust for automatic account linking # (links an SSO sign-in to an existing account with the same email). Needed when your # IdP does not assert email_verified — typically SAML providers, or OIDC providers that # omit the claim. Set it to the Provider ID you registered, then restart. @@ -295,12 +300,27 @@ SSO_TRUSTED_PROVIDER_IDS=custom-oidc,partner-saml ``` - When someone signs in with SSO and an account with the same email already exists - (for example, they previously signed up with email/password), Sim links the SSO - identity to that account automatically as long as your IdP reports the email as - verified, or the provider is trusted. If you hit an `account not linked` error, - either confirm your IdP sends `email_verified`, or add the provider's ID to - `SSO_TRUSTED_PROVIDER_IDS` and restart. + While `SSO_DOMAIN_VERIFICATION_ENABLED` is false, existing linking behavior uses + the IdP's `email_verified` claim or `SSO_TRUSTED_PROVIDER_IDS`. Once the flag is + true, Sim requires the SSO provider's organization domain to be verified and no + longer trusts an IdP-controlled email-verification claim by itself. + + + + For upgrades, leave domain verification disabled until the database migration + has landed and + `SSO_AUDIT_APPROVED_PROVIDER_IDS=reviewed-provider-ids bun run --cwd packages/db db:audit-sso-providers` + passes. The approval list records the providers whose existing account links + and active sessions operators chose to retain; omit a provider until its + links are migrated or removed and its sessions are revoked. The audit reports + linked-user and active-session counts and requires one owner organization and + one registrable domain per provider. + Remediate legacy user-scoped, public-suffix, duplicate, or overlapping rows; + explicitly backfill `domain_verified` only for domains your operators have + verified; quiesce SSO writes and rerun the audit immediately before enabling + the flag. Internal pseudo-domains are not eligible for verified-domain + enforcement. Existing internal-domain providers can remain unchanged while + the flag is off, but must move to a registrable domain before activation. You can register providers through the **Settings UI** (same as cloud) or by running the registration script directly against your database. @@ -318,6 +338,7 @@ SSO_PROVIDER_ID=okta \ SSO_ISSUER=https://dev-1234567.okta.com/oauth2/default \ SSO_DOMAIN=company.com \ SSO_USER_EMAIL=admin@company.com \ +SSO_ORGANIZATION_ID=your-organization-id \ SSO_OIDC_CLIENT_ID=your-client-id \ SSO_OIDC_CLIENT_SECRET=your-client-secret \ bun run packages/db/scripts/register-sso-provider.ts @@ -332,6 +353,7 @@ SSO_PROVIDER_ID=adfs \ SSO_ISSUER=https://your-instance.com \ SSO_DOMAIN=company.com \ SSO_USER_EMAIL=admin@company.com \ +SSO_ORGANIZATION_ID=your-organization-id \ SSO_SAML_ENTRY_POINT=https://adfs.company.com/adfs/ls \ SSO_SAML_CERT="-----BEGIN CERTIFICATE----- ... @@ -345,5 +367,7 @@ To remove a provider: ```bash SSO_USER_EMAIL=admin@company.com \ +SSO_ORGANIZATION_ID=your-organization-id \ +SSO_PROVIDER_ID=adfs \ bun run packages/db/scripts/deregister-sso-provider.ts ``` diff --git a/apps/realtime/src/env.ts b/apps/realtime/src/env.ts index 5afdf3a20f3..2f75c4a595f 100644 --- a/apps/realtime/src/env.ts +++ b/apps/realtime/src/env.ts @@ -14,6 +14,7 @@ const EnvSchema = z.object({ INTERNAL_API_SECRET: z.string().min(32), NEXT_PUBLIC_APP_URL: z.string().url(), ALLOWED_ORIGINS: z.string().optional(), + REALTIME_HOST: z.string().min(1).default('0.0.0.0'), PORT: z.coerce.number().int().positive().default(3002), SIM_DB_ROLE: z.enum(['web', 'trigger', 'realtime']).optional(), DISABLE_AUTH: z diff --git a/apps/realtime/src/index.ts b/apps/realtime/src/index.ts index e43184c1f02..0c84867156b 100644 --- a/apps/realtime/src/index.ts +++ b/apps/realtime/src/index.ts @@ -31,6 +31,7 @@ async function createRoomManager(io: SocketIOServer): Promise { async function main() { const httpServer = createServer() const PORT = env.PORT + const HOST = env.REALTIME_HOST logger.info('Starting Socket.IO server...', { port: PORT, @@ -96,9 +97,9 @@ async function main() { await assertSchemaCompatibility() - httpServer.listen(PORT, '0.0.0.0', () => { - logger.info(`Socket.IO server running on port ${PORT}`) - logger.info(`Health check available at: http://localhost:${PORT}/health`) + httpServer.listen(PORT, HOST, () => { + logger.info(`Socket.IO server running on ${HOST}:${PORT}`) + logger.info(`Health check available at: http://${HOST}:${PORT}/health`) }) const shutdown = async () => { diff --git a/apps/realtime/src/routes/http.ts b/apps/realtime/src/routes/http.ts index 0f8ed73cc52..0bfef282fcf 100644 --- a/apps/realtime/src/routes/http.ts +++ b/apps/realtime/src/routes/http.ts @@ -69,6 +69,7 @@ export function createHttpHandler(roomManager: IRoomManager, logger: Logger) { status: 'ok', timestamp: new Date().toISOString(), connections, + ...(process.env.E2E_RUN_ID ? { runId: process.env.E2E_RUN_ID } : {}), }) ) } catch (error) { diff --git a/apps/sim/app/(landing)/layout.tsx b/apps/sim/app/(landing)/layout.tsx index 9fef6524507..f9c31e12780 100644 --- a/apps/sim/app/(landing)/layout.tsx +++ b/apps/sim/app/(landing)/layout.tsx @@ -11,6 +11,7 @@ import { XPageViewTracker } from '@/app/(landing)/x-page-view-tracker' const HUBSPOT_SCRIPT_SRC = 'https://js-na2.hs-scripts.com/246720681.js' as const const X_PIXEL_ID = 'q5xbl' as const +const isMarketingAnalyticsEnabled = isHosted && !process.env.E2E_PROFILE /** X (Twitter) conversion tracking base code — loads uwt.js and fires the initial PageView. */ const X_PIXEL_BASE_CODE = `!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments); @@ -43,7 +44,7 @@ export default function LandingLayout({ children }: { children: ReactNode }) { {children} {/* HubSpot + X pixel tracking — hosted only */} - {isHosted && ( + {isMarketingAnalyticsEnabled && ( <>