diff --git a/.github/workflows/ci-test-custom-script.yml b/.github/workflows/ci-test-custom-script.yml index d29112a57aa6..fcca7faeb058 100644 --- a/.github/workflows/ci-test-custom-script.yml +++ b/.github/workflows/ci-test-custom-script.yml @@ -188,6 +188,7 @@ jobs: -e APPSMITH_CLOUD_SERVICES_BASE_URL=http://host.docker.internal:5001 \ -e APPSMITH_CLOUD_SERVICES_SIGNATURE_BASE_URL=http://host.docker.internal:8090 \ -e APPSMITH_RATE_LIMIT=1000 \ + -e APPSMITH_BASE_URL=http://localhost \ --add-host=host.docker.internal:host-gateway --add-host=api.segment.io:host-gateway --add-host=t.appsmith.com:host-gateway \ cicontainer diff --git a/.github/workflows/ci-test-limited-with-count.yml b/.github/workflows/ci-test-limited-with-count.yml index cf9be992f9bf..891d357e6cee 100644 --- a/.github/workflows/ci-test-limited-with-count.yml +++ b/.github/workflows/ci-test-limited-with-count.yml @@ -273,6 +273,7 @@ jobs: -e APPSMITH_DISABLE_TELEMETRY=true \ -e APPSMITH_INTERCOM_APP_ID=DUMMY_VALUE \ -e APPSMITH_CLOUD_SERVICES_BASE_URL=http://host.docker.internal:5001 \ + -e APPSMITH_BASE_URL=http://localhost \ --add-host=host.docker.internal:host-gateway --add-host=api.segment.io:host-gateway --add-host=t.appsmith.com:host-gateway \ cicontainer diff --git a/.github/workflows/ci-test-limited.yml b/.github/workflows/ci-test-limited.yml index 4c5b65c1ecfd..38949bdeff77 100644 --- a/.github/workflows/ci-test-limited.yml +++ b/.github/workflows/ci-test-limited.yml @@ -184,6 +184,7 @@ jobs: -e APPSMITH_DISABLE_TELEMETRY=true \ -e APPSMITH_INTERCOM_APP_ID=DUMMY_VALUE \ -e APPSMITH_CLOUD_SERVICES_BASE_URL=http://host.docker.internal:5001 \ + -e APPSMITH_BASE_URL=http://localhost \ --add-host=host.docker.internal:host-gateway --add-host=api.segment.io:host-gateway --add-host=t.appsmith.com:host-gateway \ cicontainer diff --git a/.github/workflows/ci-test-playwright.yml b/.github/workflows/ci-test-playwright.yml index b3f7d88f5894..bf08146bb757 100644 --- a/.github/workflows/ci-test-playwright.yml +++ b/.github/workflows/ci-test-playwright.yml @@ -174,6 +174,7 @@ jobs: -e APPSMITH_CLOUD_SERVICES_BASE_URL=http://host.docker.internal:5001 \ -e APPSMITH_CLOUD_SERVICES_SIGNATURE_BASE_URL=http://host.docker.internal:8090 \ -e APPSMITH_RATE_LIMIT=1000 \ + -e APPSMITH_BASE_URL=http://localhost \ -e APPSMITH_CARBON_API_BASE_PATH=https://carbon.appsmith.com \ -e APPSMITH_CARBON_API_KEY="$APPSMITH_CARBON_API_KEY" \ -e APPSMITH_AI_SERVER_MANAGED_HOSTING=true \ diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index b009e58f1376..0483e836ec05 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -98,11 +98,15 @@ export const addIndexedDBKey = (key, value) => { }; Cypress.Commands.add("stubPostHeaderReq", () => { + // GHSA-j9gf-vw2f-9hrw: server-side strict-mode now requires Origin to match + // APPSMITH_BASE_URL. Use the live Cypress baseUrl so the intercept produces a + // valid Origin (the literal string "Cypress" used to live here as a synthetic- + // traffic flag and no longer parses as a URL). cy.intercept("POST", "/api/v1/users/invite", (req) => { - req.headers["origin"] = "Cypress"; + req.headers["origin"] = Cypress.config("baseUrl"); }).as("mockPostInvite"); cy.intercept("POST", "/api/v1/applications/invite", (req) => { - req.headers["origin"] = "Cypress"; + req.headers["origin"] = Cypress.config("baseUrl"); }).as("mockPostAppInvite"); }); @@ -752,7 +756,8 @@ Cypress.Commands.add("startServerAndRoutes", () => { hostname: window.location.host, }, (req) => { - req.headers["origin"] = "Cypress"; + // GHSA-j9gf-vw2f-9hrw: send a real Origin matching APPSMITH_BASE_URL. + req.headers["origin"] = Cypress.config("baseUrl"); }, ).as("connectGitLocalRepo"); @@ -766,13 +771,17 @@ Cypress.Commands.add("startServerAndRoutes", () => { }); cy.intercept("PUT", "/api/v1/admin/env", (req) => { - req.headers["origin"] = "Cypress"; + // GHSA-j9gf-vw2f-9hrw: server-side strict-mode now requires Origin to match + // APPSMITH_BASE_URL. Use the live Cypress baseUrl instead of the legacy + // synthetic-traffic flag "Cypress" (which no longer parses as a URL). + req.headers["origin"] = Cypress.config("baseUrl"); }).as("postEnv"); cy.intercept("GET", "/settings/general").as("getGeneral"); cy.intercept("GET", "/api/v1/tenants/current").as("signUpLogin"); cy.intercept("PUT", "/api/v1/tenants", (req) => { - req.headers["origin"] = "Cypress"; + // GHSA-j9gf-vw2f-9hrw: see note above. + req.headers["origin"] = Cypress.config("baseUrl"); }).as("postTenant"); cy.intercept("PUT", "/api/v1/git/applications/*/discard").as( "discardChanges", diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 9a9640abeb18..d35ef66b79ce 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1015,6 +1015,12 @@ export const READ_DOCUMENTATION = () => "Read documentation"; export const LEARN_MORE = () => "Learn more"; export const I_UNDERSTAND = () => "I understand"; + +// Admin warning banner — shown when APPSMITH_BASE_URL is unset and the resolver is in +// fail-closed mode for token-bearing email flows. See GHSA-j9gf-vw2f-9hrw. +export const BASE_URL_MISSING_BANNER_BODY = () => + "Email delivery is disabled — forgot-password, email-verification and invite emails will not be sent until APPSMITH_BASE_URL is configured."; +export const BASE_URL_MISSING_BANNER_CTA = () => "Configure APPSMITH_BASE_URL"; export const GIT_NO_UPDATED_TOOLTIP = () => "No new updates to push"; export const FIND_OR_CREATE_A_BRANCH = () => "Find or create a branch"; diff --git a/app/client/src/components/editorComponents/BaseUrlMissingBanner.test.tsx b/app/client/src/components/editorComponents/BaseUrlMissingBanner.test.tsx new file mode 100644 index 000000000000..6323b52fba48 --- /dev/null +++ b/app/client/src/components/editorComponents/BaseUrlMissingBanner.test.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import { ThemeProvider } from "styled-components"; +import { BrowserRouter as Router } from "react-router-dom"; +import { lightTheme } from "selectors/themeSelectors"; +import BaseUrlMissingBanner from "./BaseUrlMissingBanner"; + +const mockStore = configureStore([]); + +const storeWith = (currentUser: object | null, orgConfig?: object) => + mockStore({ + ui: { users: { currentUser } }, + organization: orgConfig ? { organizationConfiguration: orgConfig } : {}, + }); + +const renderBanner = (currentUser: object | null, orgConfig?: object) => + render( + + + + + + + , + ); + +const SUPER_ADMIN = { + isSuperUser: true, + adminSettingsVisible: true, +}; + +describe("BaseUrlMissingBanner — GHSA-j9gf-vw2f-9hrw", () => { + it("renders for super-user with admin settings visible and unhealthy org config", () => { + renderBanner(SUPER_ADMIN, { instanceBaseUrlConfigurationHealthy: false }); + + expect( + screen.getByTestId("t--base-url-missing-banner"), + ).toBeInTheDocument(); + expect(screen.getByText(/Email delivery is disabled/i)).toBeInTheDocument(); + }); + + it("does not render when org config is healthy", () => { + const { container } = renderBanner(SUPER_ADMIN, { + instanceBaseUrlConfigurationHealthy: true, + }); + + expect(container.firstChild).toBeNull(); + }); + + it("does not render for non-super-user", () => { + const { container } = renderBanner( + { isSuperUser: false, adminSettingsVisible: true }, + { instanceBaseUrlConfigurationHealthy: false }, + ); + + expect(container.firstChild).toBeNull(); + }); + + it("does not render when admin settings hidden (RBAC / license guard)", () => { + const { container } = renderBanner( + { isSuperUser: true, adminSettingsVisible: false }, + { instanceBaseUrlConfigurationHealthy: false }, + ); + + expect(container.firstChild).toBeNull(); + }); + + // Rolling-deploy safety: org config without the new field. Banner must stay + // hidden, not flip on as a false positive. + it("does not render when health field is missing from org config (rolling deploy)", () => { + const { container } = renderBanner(SUPER_ADMIN, {}); + + expect(container.firstChild).toBeNull(); + }); + + it("does not render when org config is missing entirely", () => { + const { container } = renderBanner(SUPER_ADMIN); + + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/app/client/src/components/editorComponents/BaseUrlMissingBanner.tsx b/app/client/src/components/editorComponents/BaseUrlMissingBanner.tsx new file mode 100644 index 000000000000..a778c4eda1ec --- /dev/null +++ b/app/client/src/components/editorComponents/BaseUrlMissingBanner.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { useSelector } from "react-redux"; +import styled from "styled-components"; +import { Banner } from "@appsmith/ads"; +import { getShouldShowBaseUrlMissingBanner } from "selectors/usersSelectors"; +import { adminSettingsCategoryUrl } from "ee/RouteBuilder"; +import { SettingCategories } from "ee/pages/AdminSettings/config/types"; +import { + BASE_URL_MISSING_BANNER_BODY, + BASE_URL_MISSING_BANNER_CTA, + createMessage, +} from "ee/constants/messages"; + +/** + * GHSA-j9gf-vw2f-9hrw — admin warning banner shown to instance super-users when + * the server's SecureBaseUrlResolver reports that APPSMITH_BASE_URL is unset and + * token-bearing email flows are therefore disabled. + * + * Uses the same `Banner` ADS component + `position: fixed; top: 0` styling as + * the existing PageBannerMessage (license/trial banner) — single source of truth + * for top-of-screen banners across the product. Not user-dismissible: banner + * reflects live server state and clears on the next bootstrap fetch after the + * admin sets APPSMITH_BASE_URL via Admin Settings (which triggers the existing + * Configuration-tier server-restart + SPA-reload flow). + */ +const StyledBanner = styled(Banner)` + position: fixed; + z-index: 2; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + top: 0; +`; + +const BaseUrlMissingBanner: React.FC = () => { + const shouldShow = useSelector(getShouldShowBaseUrlMissingBanner); + + if (!shouldShow) return null; + + return ( + + {createMessage(BASE_URL_MISSING_BANNER_BODY)} + + ); +}; + +export default BaseUrlMissingBanner; diff --git a/app/client/src/pages/common/PageHeader.tsx b/app/client/src/pages/common/PageHeader.tsx index cc4a61b2511e..9a7d63884be7 100644 --- a/app/client/src/pages/common/PageHeader.tsx +++ b/app/client/src/pages/common/PageHeader.tsx @@ -1,7 +1,10 @@ import React, { useEffect } from "react"; import { useRouteMatch } from "react-router-dom"; import { connect, useDispatch, useSelector } from "react-redux"; -import { getCurrentUser } from "selectors/usersSelectors"; +import { + getCurrentUser, + getShouldShowBaseUrlMissingBanner, +} from "selectors/usersSelectors"; import styled from "styled-components"; import StyledHeader from "components/designSystems/appsmith/StyledHeader"; import type { DefaultRootState } from "react-redux"; @@ -10,6 +13,7 @@ import { useIsMobileDevice } from "utils/hooks/useDeviceDetect"; import { getTemplateNotificationSeenAction } from "actions/templateActions"; import { shouldShowLicenseBanner } from "ee/selectors/organizationSelectors"; import { Banner } from "ee/utils/licenseHelpers"; +import BaseUrlMissingBanner from "components/editorComponents/BaseUrlMissingBanner"; import bootIntercom from "utils/bootIntercom"; import EntitySearchBar from "pages/common/SearchBar/EntitySearchBar"; @@ -61,14 +65,23 @@ export function PageHeader(props: PageHeaderProps) { const showBanner = useSelector(shouldShowLicenseBanner); const isHomePage = useRouteMatch("/applications")?.isExact; const isLicensePage = useRouteMatch("/license")?.isExact; + // GHSA-j9gf-vw2f-9hrw — fold the base-url-missing admin banner into the same + // isBannerVisible signal that the existing license/trial banner uses, so the + // page header gets pushed down by the banner's height (40px desktop / 70px + // mobile) when either banner is visible. Without this, the page header — which + // has a higher z-index — paints over the fixed-position banner at top: 0. + const showBaseUrlBanner = useSelector(getShouldShowBaseUrlMissingBanner); + const isAnyBannerVisible = + (showBanner && (isHomePage || isLicensePage)) || showBaseUrlBanner; return ( <> + diff --git a/app/client/src/selectors/usersSelectors.test.ts b/app/client/src/selectors/usersSelectors.test.ts new file mode 100644 index 000000000000..24ec5ec9f512 --- /dev/null +++ b/app/client/src/selectors/usersSelectors.test.ts @@ -0,0 +1,82 @@ +import { getShouldShowBaseUrlMissingBanner } from "./usersSelectors"; + +// Minimal Redux state shape; the selector reads ui.users.currentUser AND +// organization.organizationConfiguration. +const stateWith = ( + currentUser: object | null, + orgConfig: object | undefined = undefined, +) => + ({ + ui: { users: { currentUser } }, + organization: orgConfig + ? { organizationConfiguration: orgConfig } + : undefined, + }) as never; + +const SUPER_ADMIN = { + isSuperUser: true, + adminSettingsVisible: true, +}; + +describe("getShouldShowBaseUrlMissingBanner — GHSA-j9gf-vw2f-9hrw", () => { + it("returns true for super user with admin settings visible and unhealthy org config", () => { + expect( + getShouldShowBaseUrlMissingBanner( + stateWith(SUPER_ADMIN, { instanceBaseUrlConfigurationHealthy: false }), + ), + ).toBe(true); + }); + + it("returns false when org config is healthy", () => { + expect( + getShouldShowBaseUrlMissingBanner( + stateWith(SUPER_ADMIN, { instanceBaseUrlConfigurationHealthy: true }), + ), + ).toBe(false); + }); + + it("returns false for non-super-user", () => { + expect( + getShouldShowBaseUrlMissingBanner( + stateWith( + { isSuperUser: false, adminSettingsVisible: true }, + { instanceBaseUrlConfigurationHealthy: false }, + ), + ), + ).toBe(false); + }); + + it("returns false when admin settings hidden (RBAC / license guard)", () => { + expect( + getShouldShowBaseUrlMissingBanner( + stateWith( + { isSuperUser: true, adminSettingsVisible: false }, + { instanceBaseUrlConfigurationHealthy: false }, + ), + ), + ).toBe(false); + }); + + // Rolling-deploy safety: newer client briefly paired with older server has the + // health field absent on org config. Banner must stay hidden, not flip on as a + // false positive. + it("returns false when health field is missing from org config (rolling deploy)", () => { + expect(getShouldShowBaseUrlMissingBanner(stateWith(SUPER_ADMIN, {}))).toBe( + false, + ); + }); + + it("returns false when org config is missing entirely", () => { + expect(getShouldShowBaseUrlMissingBanner(stateWith(SUPER_ADMIN))).toBe( + false, + ); + }); + + it("returns false when currentUser is null", () => { + expect( + getShouldShowBaseUrlMissingBanner( + stateWith(null, { instanceBaseUrlConfigurationHealthy: false }), + ), + ).toBe(false); + }); +}); diff --git a/app/client/src/selectors/usersSelectors.tsx b/app/client/src/selectors/usersSelectors.tsx index 00a36332e49b..86a271988c03 100644 --- a/app/client/src/selectors/usersSelectors.tsx +++ b/app/client/src/selectors/usersSelectors.tsx @@ -27,3 +27,34 @@ export const getFeatureFlagsFetching = (state: DefaultRootState) => export const getIsUserLoggedIn = (state: DefaultRootState): boolean => state.ui.users.currentUser?.email !== ANONYMOUS_USERNAME; + +/** + * GHSA-j9gf-vw2f-9hrw — admin warning banner gate. Returns true only when ALL of: + * - the current user is an instance super user (user-level gate), + * - admin settings are visible to them (RBAC / license tier guard), + * - and the server explicitly reports the org config's + * `instanceBaseUrlConfigurationHealthy === false` (instance-level signal). + * + * The instance signal lives on `state.organization.organizationConfiguration` + * (populated from the always-called `/v1/consolidated-api` bootstrap), not on + * `currentUser`. `/v1/users/me` is rarely re-fetched on dashboard routes — the + * canonical bootstrap is consolidated-api, and org config is the right bucket + * within it for instance-state. + * + * The explicit `=== false` (rather than `!value`) is deliberate: during a rolling + * deploy where a newer client briefly sees an older server's response without the + * field, `undefined === false` is `false`, so the banner stays hidden until both + * sides are deployed. + */ +export const getShouldShowBaseUrlMissingBanner = ( + state: DefaultRootState, +): boolean => { + const user = state.ui?.users?.currentUser; + const orgConfig = state.organization?.organizationConfiguration; + + return Boolean( + user?.isSuperUser && + user?.adminSettingsVisible && + orgConfig?.instanceBaseUrlConfigurationHealthy === false, + ); +}; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/OrganizationConfigurationCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/OrganizationConfigurationCE.java index 0b6ee27834ba..e2ed0d115aea 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/OrganizationConfigurationCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/OrganizationConfigurationCE.java @@ -63,6 +63,18 @@ public class OrganizationConfigurationCE implements Serializable { private Boolean isAtomicPushAllowed = false; + /** + * Health signal driving the admin warning banner introduced for + * GHSA-j9gf-vw2f-9hrw. + * Set at runtime by {@code OrganizationServiceCEImpl#getClientPertinentOrganization} from + * {@code SecureBaseUrlResolverCE#isBaseUrlConfigurationHealthy()} — this field is NOT + * persisted to the DB, so it does not appear in {@link #copyNonSensitiveValues}. False on + * a CE / single-org-EE deployment with {@code APPSMITH_BASE_URL} unset; always true on + * multi-org-EE because each organization derives its own canonical base URL. + */ + @Transient + private Boolean instanceBaseUrlConfigurationHealthy; + public void addThirdPartyAuth(String auth) { if (thirdPartyAuths == null) { thirdPartyAuths = new ArrayList<>(); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index 82981b55d7bc..7c8bf5219f19 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -893,6 +893,15 @@ public enum AppsmithError { ErrorType.INTERNAL_ERROR, null), + MISCONFIGURED_INSTANCE_BASE_URL( + 500, + AppsmithErrorCode.MISCONFIGURED_INSTANCE_BASE_URL.getCode(), + "APPSMITH_BASE_URL is not configured. Token-bearing email flows are disabled until the canonical instance URL is set.", + AppsmithErrorAction.DEFAULT, + "Instance base URL not configured", + ErrorType.INTERNAL_ERROR, + null), + INVALID_SMTP_CONFIGURATION( 400, AppsmithErrorCode.INVALID_SMTP_CONFIGURATION.getCode(), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java index 6f1b759e1ea5..a9cd4e68ce8a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java @@ -64,6 +64,7 @@ public enum AppsmithErrorCode { GOOGLE_RECAPTCHA_TIMEOUT("AE-APP-5042", "Google recaptcha timeout"), MIGRATION_FAILED("AE-APP-5043", "Migration failed"), INVALID_PROPERTIES_CONFIGURATION("AE-APP-5044", "Property configuration is wrong or malformed"), + MISCONFIGURED_INSTANCE_BASE_URL("AE-APP-5048", "Instance base URL is not configured"), NAME_CLASH_NOT_ALLOWED_IN_REFACTOR("AE-AST-4009", "Name clash not allowed in refactor"), GENERIC_BAD_REQUEST("AE-BAD-4000", "Generic bad request"), MALFORMED_REQUEST("AE-BAD-4001", "Malformed request body"), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SecureBaseUrlResolver.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SecureBaseUrlResolver.java new file mode 100644 index 000000000000..72e4c5aa1db5 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SecureBaseUrlResolver.java @@ -0,0 +1,10 @@ +package com.appsmith.server.helpers; + +import com.appsmith.server.helpers.ce.SecureBaseUrlResolverCE; + +/** + * Marker interface used by Spring DI. CE provides a default + * {@link com.appsmith.server.helpers.SecureBaseUrlResolverImpl}; EE overrides the + * implementation class to add multi-org-aware resolution. + */ +public interface SecureBaseUrlResolver extends SecureBaseUrlResolverCE {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SecureBaseUrlResolverImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SecureBaseUrlResolverImpl.java new file mode 100644 index 000000000000..30bfe340bddf --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/SecureBaseUrlResolverImpl.java @@ -0,0 +1,12 @@ +package com.appsmith.server.helpers; + +import com.appsmith.server.helpers.ce.SecureBaseUrlResolverCEImpl; +import org.springframework.stereotype.Component; + +/** + * CE concrete bean for {@link SecureBaseUrlResolver}. EE replaces this class with + * a multi-org-aware variant that derives the trusted host from the organization + * configuration when the {@code license_multi_org_enabled} feature flag is on. + */ +@Component +public class SecureBaseUrlResolverImpl extends SecureBaseUrlResolverCEImpl implements SecureBaseUrlResolver {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCE.java new file mode 100644 index 000000000000..0adc4bb452bc --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCE.java @@ -0,0 +1,55 @@ +package com.appsmith.server.helpers.ce; + +import reactor.core.publisher.Mono; + +/** + * Resolves the trusted, server-side base URL used as the host of token-bearing + * email links (forgot-password, email verification, workspace invite, + * instance-admin invite). + * + *

The resolver MUST NOT trust request-supplied values such as the {@code Origin} + * header as the canonical host. Doing so allows an unauthenticated attacker to + * influence the link host of security-sensitive emails — see + * GHSA-j9gf-vw2f-9hrw. + * + *

Implementations return {@link Mono#empty()} when no trusted base URL can be + * resolved and the insecure compatibility flag is off. Callers are expected to + * translate this into a flow-appropriate response: + *

    + *
  • For unauthenticated anti-enumeration flows (forgot password, resend + * verification): return a generic success response without sending email.
  • + *
  • For authenticated flows (workspace invite, instance-admin invite): surface + * a clear configuration error to the admin caller.
  • + *
+ */ +public interface SecureBaseUrlResolverCE { + + /** + * @param providedBaseUrl the base URL extracted from the inbound request (typically + * the {@code Origin} header). Treated as untrusted input. + * @return a {@link Mono} emitting the resolved trusted base URL, or empty if no + * trusted URL can be resolved and the insecure compatibility fallback is off. + * May emit an error if a configured base URL is set and the provided value + * does not match (strict-mode enforcement, preserved from prior hardening). + */ + Mono resolveSecureBaseUrl(String providedBaseUrl); + + /** + * Reports whether this instance is in a state where token-bearing email flows + * (forgot-password, email verification, invites) can generate links without + * depending on a request-time hint such as the {@code Origin} header. + * + *

The CE implementation returns {@code true} iff {@code APPSMITH_BASE_URL} is set; + * the EE override returns {@code true} unconditionally when the multi-org feature flag + * is on (each organization derives its own canonical URL from slug + deploymentDomain). + * + *

Drives the in-product admin warning banner shown to instance super-users. The + * insecure-flag fallback intentionally does NOT mark the instance as healthy — operators + * who opted into the deprecated {@code APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS} escape + * hatch should still see the warning so the deprecation pressure is preserved. + * + * @return {@code Mono} emitting {@code true} when no banner should be shown, + * {@code false} when the banner should warn that token-bearing emails are disabled. + */ + Mono isBaseUrlConfigurationHealthy(); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCEImpl.java new file mode 100644 index 000000000000..dbfc7cc5061b --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCEImpl.java @@ -0,0 +1,145 @@ +package com.appsmith.server.helpers.ce; + +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; +import java.util.Objects; + +/** + * CE implementation of {@link SecureBaseUrlResolverCE}. + * + *

Resolution rules: + * + *

    + *
  1. If {@code APPSMITH_BASE_URL} is configured, it is the only acceptable host. If + * the provided value does not match (compared by URL origin — scheme + host + effective port, + * per RFC 6454), an {@link AppsmithException} is emitted. This preserves the strict-mode + * protection added in PR #41426 for + * GHSA-7hf5-mc28-xmcv. + * The insecure compatibility flag does NOT weaken this branch.
  2. + *
  3. If {@code APPSMITH_BASE_URL} is unset and {@code APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS} + * is true, the legacy behaviour is restored: the caller-supplied value is + * returned. A WARN log is emitted on every call so operators are aware they + * are running in an insecure mode. This flag is intended only as a transition + * path and is documented as deprecated.
  4. + *
  5. If {@code APPSMITH_BASE_URL} is unset and the insecure flag is off (the new + * default), the resolver emits {@link Mono#empty()} together with a WARN log. + * Callers in unauthenticated flows convert this into a generic success + * response without dispatching email (anti-enumeration). Callers in + * authenticated flows convert it into an explicit configuration error.
  6. + *
+ */ +@Slf4j +public class SecureBaseUrlResolverCEImpl implements SecureBaseUrlResolverCE { + + @Value("${APPSMITH_BASE_URL:}") + private String appsmithBaseUrl; + + /** + * Opt-in escape hatch for legacy self-hosted deployments that have not yet + * configured {@code APPSMITH_BASE_URL}. When true, the resolver falls back to + * the caller-supplied value (the historical, insecure behaviour). Defaults to + * false. Intended only as a temporary migration window — set + * {@code APPSMITH_BASE_URL} to your instance's canonical URL and remove this + * flag. + */ + @Value("${APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS:false}") + private boolean allowInsecureOriginBasedLinks; + + @Override + public Mono resolveSecureBaseUrl(String providedBaseUrl) { + if (StringUtils.hasText(appsmithBaseUrl)) { + if (!sameOrigin(appsmithBaseUrl, providedBaseUrl)) { + log.warn( + "Origin mismatch: provided='{}' does not match configured APPSMITH_BASE_URL='{}'.", + providedBaseUrl, + appsmithBaseUrl); + return Mono.error(new AppsmithException( + AppsmithError.GENERIC_BAD_REQUEST, + "Origin header does not match APPSMITH_BASE_URL configuration.")); + } + return Mono.just(appsmithBaseUrl); + } + + if (allowInsecureOriginBasedLinks) { + log.warn("APPSMITH_BASE_URL is not configured and APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS=true. " + + "Token-bearing email links will be derived from the request Origin header. " + + "This is INSECURE and intended only as a transition path. " + + "Set APPSMITH_BASE_URL to your instance's canonical URL and remove the insecure flag."); + return Mono.just(providedBaseUrl); + } + + log.warn("APPSMITH_BASE_URL is not configured. Token-bearing email flows (password reset, " + + "email verification, invites) are disabled until APPSMITH_BASE_URL is set. " + + "See https://github.com/appsmithorg/appsmith/security/advisories/GHSA-j9gf-vw2f-9hrw"); + return Mono.empty(); + } + + @Override + public Mono isBaseUrlConfigurationHealthy() { + return Mono.just(StringUtils.hasText(appsmithBaseUrl)); + } + + /** + * Compares two URLs by their origin (scheme + host + effective port) per RFC 6454, rather than + * by raw string equality. Tolerates insignificant differences such as trailing slashes and + * default-port elision (e.g. {@code http://example.com} and {@code http://example.com:80} are + * the same origin) while still rejecting any difference in scheme, host, or non-default port. + * + *

Returns {@code false} for any malformed URL or null/blank input. Userinfo, path, query, + * and fragment are deliberately ignored — only the security-relevant origin is compared. + */ + private static boolean sameOrigin(String configured, String provided) { + if (!StringUtils.hasText(configured) || !StringUtils.hasText(provided)) { + return false; + } + try { + URI configuredUri = new URI(configured.trim()); + URI providedUri = new URI(provided.trim()); + + String configuredScheme = lowerCase(configuredUri.getScheme()); + String providedScheme = lowerCase(providedUri.getScheme()); + if (!Objects.equals(configuredScheme, providedScheme)) { + return false; + } + + String configuredHost = lowerCase(configuredUri.getHost()); + String providedHost = lowerCase(providedUri.getHost()); + if (configuredHost == null || !Objects.equals(configuredHost, providedHost)) { + // Reject when host can't be parsed (e.g. opaque URIs) or hosts differ. + return false; + } + + return effectivePort(configuredUri) == effectivePort(providedUri); + } catch (URISyntaxException e) { + log.warn("Failed to parse URL for origin comparison: {}", e.getMessage()); + return false; + } + } + + private static int effectivePort(URI uri) { + int port = uri.getPort(); + if (port != -1) { + return port; + } + String scheme = lowerCase(uri.getScheme()); + if ("http".equals(scheme)) { + return 80; + } + if ("https".equals(scheme)) { + return 443; + } + return -1; + } + + private static String lowerCase(String value) { + return value == null ? null : value.toLowerCase(Locale.ROOT); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java index 437a8287ff80..bea886725833 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ConsolidatedAPIServiceImpl.java @@ -4,6 +4,7 @@ import com.appsmith.server.actioncollections.base.ActionCollectionService; import com.appsmith.server.applications.base.ApplicationService; import com.appsmith.server.datasources.base.DatasourceService; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.jslibs.base.CustomJSLibService; import com.appsmith.server.newactions.base.NewActionService; import com.appsmith.server.newpages.base.NewPageService; @@ -39,7 +40,8 @@ public ConsolidatedAPIServiceImpl( MockDataService mockDataService, ObservationRegistry observationRegistry, CacheableRepositoryHelper cacheableRepositoryHelper, - ObservationHelper observationHelper) { + ObservationHelper observationHelper, + SecureBaseUrlResolver secureBaseUrlResolver) { super( sessionUserService, userService, @@ -59,6 +61,7 @@ public ConsolidatedAPIServiceImpl( mockDataService, observationRegistry, cacheableRepositoryHelper, - observationHelper); + observationHelper, + secureBaseUrlResolver); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EmailServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EmailServiceImpl.java index 7e4d440d3ac2..0d6fe04f1991 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EmailServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EmailServiceImpl.java @@ -1,13 +1,17 @@ package com.appsmith.server.services; import com.appsmith.server.helpers.EmailServiceHelper; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.notifications.EmailSender; import com.appsmith.server.services.ce.EmailServiceCEImpl; import org.springframework.stereotype.Service; @Service public class EmailServiceImpl extends EmailServiceCEImpl implements EmailService { - public EmailServiceImpl(EmailSender emailSender, EmailServiceHelper emailServiceHelper) { - super(emailSender, emailServiceHelper); + public EmailServiceImpl( + EmailSender emailSender, + EmailServiceHelper emailServiceHelper, + SecureBaseUrlResolver secureBaseUrlResolver) { + super(emailSender, emailServiceHelper, secureBaseUrlResolver); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java index 75d59671c003..6c8683fb4cf4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java @@ -2,6 +2,7 @@ import com.appsmith.server.configurations.CommonConfig; import com.appsmith.server.configurations.EmailConfig; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.helpers.UserServiceHelper; import com.appsmith.server.helpers.UserUtils; import com.appsmith.server.instanceconfigs.helpers.InstanceVariablesHelper; @@ -44,7 +45,8 @@ public UserServiceImpl( RateLimitService rateLimitService, PACConfigurationService pacConfigurationService, UserServiceHelper userServiceHelper, - InstanceVariablesHelper instanceVariablesHelper) { + InstanceVariablesHelper instanceVariablesHelper, + SecureBaseUrlResolver secureBaseUrlResolver) { super( validator, repository, @@ -62,6 +64,7 @@ public UserServiceImpl( rateLimitService, pacConfigurationService, userServiceHelper, - instanceVariablesHelper); + instanceVariablesHelper, + secureBaseUrlResolver); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java index 4f579b36e018..ee2609360250 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceCEImpl.java @@ -12,6 +12,7 @@ import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.GitArtifactMetadata; import com.appsmith.server.domains.NewPage; +import com.appsmith.server.domains.OrganizationConfiguration; import com.appsmith.server.domains.Plugin; import com.appsmith.server.dtos.ApplicationPagesDTO; import com.appsmith.server.dtos.ConsolidatedAPIResponseDTO; @@ -21,6 +22,7 @@ import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.GitUtils; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.helpers.TextUtils; import com.appsmith.server.jslibs.base.CustomJSLibService; import com.appsmith.server.newactions.base.NewActionService; @@ -128,6 +130,7 @@ public class ConsolidatedAPIServiceCEImpl implements ConsolidatedAPIServiceCE { private final ObservationRegistry observationRegistry; private final CacheableRepositoryHelper cacheableRepositoryHelper; private final ObservationHelper observationHelper; + private final SecureBaseUrlResolver secureBaseUrlResolver; protected ResponseDTO getSuccessResponse(T data) { return new ResponseDTO<>(HttpStatus.OK, data); @@ -207,9 +210,28 @@ protected List> getAllFetchableMonos( .cache(); fetches.add(featureFlagsForCurrentUserResponseDTOMonoCache); - /* Get organization config data */ + /* Get organization config data, then enrich with the runtime + * `instanceBaseUrlConfigurationHealthy` signal that drives the admin warning banner + * for GHSA-j9gf-vw2f-9hrw. The resolver call is intentionally co-located here in the + * orchestration layer rather than inside OrganizationServiceCEImpl: in EE the resolver + * depends on FeatureFlagService which depends on OrganizationService, so injecting it + * into the org service would create a construction-time cycle. Co-locating in the + * consumer (consolidated-api) keeps the data layer cycle-free. + * .onErrorReturn(true) so a transient resolver/feature-flag failure produces a + * false-negative banner rather than breaking the org-config fetch entirely. */ fetches.add(organizationService .getOrganizationConfiguration() + .zipWith(secureBaseUrlResolver.isBaseUrlConfigurationHealthy().onErrorReturn(true)) + .map(tuple -> { + var organization = tuple.getT1(); + // Defensive: org-service production code always populates the config, but + // some test fixtures and mocks return a bare Organization. Don't NPE. + if (organization.getOrganizationConfiguration() == null) { + organization.setOrganizationConfiguration(new OrganizationConfiguration()); + } + organization.getOrganizationConfiguration().setInstanceBaseUrlConfigurationHealthy(tuple.getT2()); + return organization; + }) .as(this::toResponseDTO) .doOnError(e -> log.error("Error fetching organization config", e)) .doOnSuccess(consolidatedAPIResponseDTO::setOrganizationConfig) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/EmailServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/EmailServiceCEImpl.java index dca7285e8382..009455d1485b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/EmailServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/EmailServiceCEImpl.java @@ -4,7 +4,10 @@ import com.appsmith.server.domains.PermissionGroup; import com.appsmith.server.domains.User; import com.appsmith.server.domains.Workspace; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.EmailServiceHelper; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.notifications.EmailSender; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -23,13 +26,23 @@ public class EmailServiceCEImpl implements EmailServiceCE { private final EmailServiceHelper emailServiceHelper; - public EmailServiceCEImpl(EmailSender emailSender, EmailServiceHelper emailServiceHelper) { + private final SecureBaseUrlResolver secureBaseUrlResolver; + + public EmailServiceCEImpl( + EmailSender emailSender, + EmailServiceHelper emailServiceHelper, + SecureBaseUrlResolver secureBaseUrlResolver) { this.emailSender = emailSender; this.emailServiceHelper = emailServiceHelper; + this.secureBaseUrlResolver = secureBaseUrlResolver; } @Override public Mono sendForgotPasswordEmail(String email, String resetUrl, String originHeader) { + // The reset URL has already been built with the trusted base URL by UserServiceCEImpl + // (which routes through SecureBaseUrlResolver). The originHeader passed here is used + // only for cosmetic branding lookups; this method is a no-op when not invoked through + // that resolved path. Map params = new HashMap<>(); params.put(RESET_URL, resetUrl); return emailServiceHelper @@ -54,30 +67,42 @@ public Mono sendInviteUserToWorkspaceEmail( PermissionGroup assignedPermissionGroup, String originHeader, boolean isNewUser) { - String inviteUrl = isNewUser - ? String.format( - INVITE_USER_CLIENT_URL_FORMAT, - originHeader, - URLEncoder.encode(invitedUser.getUsername().toLowerCase(), StandardCharsets.UTF_8)) - : originHeader; - Mono emailSubjectMono = emailServiceHelper.getSubjectJoinWorkspace(workspaceInvitedTo.getName()); - Mono workspaceInviteTemplateMono = emailServiceHelper.getWorkspaceInviteTemplate(isNewUser); - Map params = getInviteToWorkspaceEmailParams( - workspaceInvitedTo, invitingUser, inviteUrl, assignedPermissionGroup.getName(), isNewUser); - return emailServiceHelper - .enrichWithBrandParams(params, originHeader) - .zipWith(Mono.zip(emailSubjectMono, workspaceInviteTemplateMono)) - .flatMap(objects -> { - Map updatedParams = objects.getT1(); - String emailSubject = objects.getT2().getT1(); - String workspaceInviteTemplate = objects.getT2().getT2(); - return emailSender.sendMail( - invitedUser.getEmail(), emailSubject, workspaceInviteTemplate, updatedParams); + // Resolve the trusted base URL through the canonical resolver. The invite flow is + // authenticated, so a missing APPSMITH_BASE_URL must surface as an explicit + // configuration error to the admin caller (rather than the silent-success behavior + // used by anti-enumeration flows). See GHSA-j9gf-vw2f-9hrw. + return secureBaseUrlResolver + .resolveSecureBaseUrl(originHeader) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.MISCONFIGURED_INSTANCE_BASE_URL))) + .flatMap(trustedBaseUrl -> { + String inviteUrl = isNewUser + ? String.format( + INVITE_USER_CLIENT_URL_FORMAT, + trustedBaseUrl, + URLEncoder.encode(invitedUser.getUsername().toLowerCase(), StandardCharsets.UTF_8)) + : trustedBaseUrl; + Mono emailSubjectMono = + emailServiceHelper.getSubjectJoinWorkspace(workspaceInvitedTo.getName()); + Mono workspaceInviteTemplateMono = emailServiceHelper.getWorkspaceInviteTemplate(isNewUser); + Map params = getInviteToWorkspaceEmailParams( + workspaceInvitedTo, invitingUser, inviteUrl, assignedPermissionGroup.getName(), isNewUser); + return emailServiceHelper + .enrichWithBrandParams(params, trustedBaseUrl) + .zipWith(Mono.zip(emailSubjectMono, workspaceInviteTemplateMono)) + .flatMap(objects -> { + Map updatedParams = objects.getT1(); + String emailSubject = objects.getT2().getT1(); + String workspaceInviteTemplate = objects.getT2().getT2(); + return emailSender.sendMail( + invitedUser.getEmail(), emailSubject, workspaceInviteTemplate, updatedParams); + }); }); } @Override public Mono sendEmailVerificationEmail(User user, String verificationURL, String originHeader) { + // The verification URL has already been built with the trusted base URL by + // UserServiceCEImpl. The originHeader is used only for branding lookups. Map params = new HashMap<>(); params.put(EMAIL_VERIFICATION_URL, verificationURL); return emailServiceHelper @@ -97,36 +122,44 @@ public Mono sendEmailVerificationEmail(User user, String verificationUR @Override public Mono sendInstanceAdminInviteEmail( User invitedUser, User invitingUser, String originHeader, boolean isNewUser) { - Map params = new HashMap<>(); - String inviteUrl = isNewUser - ? String.format( - INVITE_USER_CLIENT_URL_FORMAT, - originHeader, - URLEncoder.encode(invitedUser.getUsername().toLowerCase(), StandardCharsets.UTF_8)) - : originHeader; - params.put(PRIMARY_LINK_URL, inviteUrl); + // Auth-gated admin invite flow: resolve the trusted base URL or surface a + // configuration error. See GHSA-j9gf-vw2f-9hrw. + return secureBaseUrlResolver + .resolveSecureBaseUrl(originHeader) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.MISCONFIGURED_INSTANCE_BASE_URL))) + .flatMap(trustedBaseUrl -> { + Map params = new HashMap<>(); + String inviteUrl = isNewUser + ? String.format( + INVITE_USER_CLIENT_URL_FORMAT, + trustedBaseUrl, + URLEncoder.encode(invitedUser.getUsername().toLowerCase(), StandardCharsets.UTF_8)) + : trustedBaseUrl; + params.put(PRIMARY_LINK_URL, inviteUrl); - Mono primaryLinkTextMono = emailServiceHelper.getJoinInstanceCtaPrimaryText(); + Mono primaryLinkTextMono = emailServiceHelper.getJoinInstanceCtaPrimaryText(); - if (invitingUser != null) { - params.put(INVITER_FIRST_NAME, StringUtils.defaultIfEmpty(invitingUser.getName(), invitingUser.getEmail())); - } - return primaryLinkTextMono - .flatMap(primaryLinkText -> { - params.put(PRIMARY_LINK_TEXT, primaryLinkText); - return emailServiceHelper.enrichWithBrandParams(params, originHeader); - }) - .zipWhen(updatedParams -> { - return Mono.zip( - emailServiceHelper.getSubjectJoinInstanceAsAdmin(updatedParams.get(INSTANCE_NAME)), - emailServiceHelper.getAdminInstanceInviteTemplate()); - }) - .flatMap(objects -> { - Map updatedParams = objects.getT1(); - String subject = objects.getT2().getT1(); - String adminInstanceInviteTemplate = objects.getT2().getT2(); - return emailSender.sendMail( - invitedUser.getEmail(), subject, adminInstanceInviteTemplate, updatedParams); + if (invitingUser != null) { + params.put( + INVITER_FIRST_NAME, + StringUtils.defaultIfEmpty(invitingUser.getName(), invitingUser.getEmail())); + } + return primaryLinkTextMono + .flatMap(primaryLinkText -> { + params.put(PRIMARY_LINK_TEXT, primaryLinkText); + return emailServiceHelper.enrichWithBrandParams(params, trustedBaseUrl); + }) + .zipWhen(updatedParams -> Mono.zip( + emailServiceHelper.getSubjectJoinInstanceAsAdmin(updatedParams.get(INSTANCE_NAME)), + emailServiceHelper.getAdminInstanceInviteTemplate())) + .flatMap(objects -> { + Map updatedParams = objects.getT1(); + String subject = objects.getT2().getT1(); + String adminInstanceInviteTemplate = + objects.getT2().getT2(); + return emailSender.sendMail( + invitedUser.getEmail(), subject, adminInstanceInviteTemplate, updatedParams); + }); }); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java index ce8c30bec457..af8436841dcb 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java @@ -24,6 +24,7 @@ import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.EmailNormalizer; import com.appsmith.server.helpers.RedirectHelper; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.helpers.UserServiceHelper; import com.appsmith.server.helpers.UserUtils; import com.appsmith.server.instanceconfigs.helpers.InstanceVariablesHelper; @@ -46,7 +47,6 @@ import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.net.WWWFormCodec; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -110,41 +110,7 @@ public class UserServiceCEImpl extends BaseService private final UserServiceHelper userPoliciesComputeHelper; private final InstanceVariablesHelper instanceVariablesHelper; - - @Value("${APPSMITH_BASE_URL:}") - private String appsmithBaseUrl; - - /** - * Resolves and validates the base URL for security-sensitive operations like password reset - * and email verification. This method ensures that URLs in emails point to trusted domains. - * - *

In single-org (CE) mode: - *

    - *
  • If APPSMITH_BASE_URL is configured, validates that the provided URL matches it
  • - *
  • If APPSMITH_BASE_URL is not configured, uses the provided URL (backward compatibility)
  • - *
- * - *

This method can be overridden in EE to handle multi-org setups where each organization - * has its own base URL. - * - * @param providedBaseUrl The base URL from the request (typically from Origin header) - * @return Mono The validated/resolved base URL to use for constructing email links - */ - protected Mono resolveSecureBaseUrl(String providedBaseUrl) { - // If APPSMITH_BASE_URL is not configured, use provided URL for backwards compatibility - if (!StringUtils.hasText(appsmithBaseUrl)) { - return Mono.just(providedBaseUrl); - } - - // If APPSMITH_BASE_URL is configured, validate that Origin header matches it - if (!appsmithBaseUrl.equals(providedBaseUrl)) { - return Mono.error(new AppsmithException( - AppsmithError.GENERIC_BAD_REQUEST, - "Origin header does not match APPSMITH_BASE_URL configuration.")); - } - - return Mono.just(appsmithBaseUrl); - } + private final SecureBaseUrlResolver secureBaseUrlResolver; protected static final WebFilterChain EMPTY_WEB_FILTER_CHAIN = serverWebExchange -> Mono.empty(); private static final String FORGOT_PASSWORD_CLIENT_URL_FORMAT = "%s/user/resetPassword?token=%s"; @@ -176,7 +142,8 @@ public UserServiceCEImpl( RateLimitService rateLimitService, PACConfigurationService pacConfigurationService, UserServiceHelper userServiceHelper, - InstanceVariablesHelper instanceVariablesHelper) { + InstanceVariablesHelper instanceVariablesHelper, + SecureBaseUrlResolver secureBaseUrlResolver) { super(validator, repository, analyticsService); this.workspaceService = workspaceService; @@ -193,6 +160,7 @@ public UserServiceCEImpl( this.userPoliciesComputeHelper = userServiceHelper; this.pacConfigurationService = pacConfigurationService; this.instanceVariablesHelper = instanceVariablesHelper; + this.secureBaseUrlResolver = secureBaseUrlResolver; } @Override @@ -226,13 +194,17 @@ public Mono forgotPasswordTokenGenerate(ResetUserPasswordDTO resetUserP return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORIGIN)); } - // Resolve the secure base URL (validates in single-org, may be overridden for multi-org) - return resolveSecureBaseUrl(resetUserPasswordDTO.getBaseUrl()).flatMap(secureBaseUrl -> { - // Use the resolved secure base URL instead of the client-provided one - resetUserPasswordDTO.setBaseUrl(secureBaseUrl); - String email = resetUserPasswordDTO.getEmail(); - return processForgotPasswordTokenGeneration(email, resetUserPasswordDTO); - }); + // Resolve the secure base URL through the trusted resolver. When APPSMITH_BASE_URL is unset + // (and the insecure compatibility flag is off) the resolver returns Mono.empty(), causing + // this entire chain to complete without dispatching email — preserving the generic success + // response that anti-enumeration relies on. See GHSA-j9gf-vw2f-9hrw. + return secureBaseUrlResolver + .resolveSecureBaseUrl(resetUserPasswordDTO.getBaseUrl()) + .flatMap(secureBaseUrl -> { + resetUserPasswordDTO.setBaseUrl(secureBaseUrl); + String email = resetUserPasswordDTO.getEmail(); + return processForgotPasswordTokenGeneration(email, resetUserPasswordDTO); + }); } private Mono processForgotPasswordTokenGeneration( @@ -868,13 +840,16 @@ public Mono resendEmailVerification( return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORIGIN)); } - // Resolve the secure base URL (validates in single-org, may be overridden for multi-org) - return resolveSecureBaseUrl(resendEmailVerificationDTO.getBaseUrl()).flatMap(secureBaseUrl -> { - // Use the resolved secure base URL instead of the client-provided one - resendEmailVerificationDTO.setBaseUrl(secureBaseUrl); - String email = resendEmailVerificationDTO.getEmail(); - return processResendEmailVerification(email, resendEmailVerificationDTO, redirectUrl); - }); + // Resolve the secure base URL through the trusted resolver. When APPSMITH_BASE_URL is unset + // (and the insecure compatibility flag is off) the resolver returns Mono.empty(), causing + // this entire chain to complete without dispatching email. See GHSA-j9gf-vw2f-9hrw. + return secureBaseUrlResolver + .resolveSecureBaseUrl(resendEmailVerificationDTO.getBaseUrl()) + .flatMap(secureBaseUrl -> { + resendEmailVerificationDTO.setBaseUrl(secureBaseUrl); + String email = resendEmailVerificationDTO.getEmail(); + return processResendEmailVerification(email, resendEmailVerificationDTO, redirectUrl); + }); } private Mono processResendEmailVerification( diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/ConsolidatedAPIServiceCECompatibleImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/ConsolidatedAPIServiceCECompatibleImpl.java index f7cdeadd9115..bddd4369195d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/ConsolidatedAPIServiceCECompatibleImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/ConsolidatedAPIServiceCECompatibleImpl.java @@ -4,6 +4,7 @@ import com.appsmith.server.actioncollections.base.ActionCollectionService; import com.appsmith.server.applications.base.ApplicationService; import com.appsmith.server.datasources.base.DatasourceService; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.jslibs.base.CustomJSLibService; import com.appsmith.server.newactions.base.NewActionService; import com.appsmith.server.newpages.base.NewPageService; @@ -42,7 +43,8 @@ public ConsolidatedAPIServiceCECompatibleImpl( MockDataService mockDataService, ObservationRegistry observationRegistry, CacheableRepositoryHelper cacheableRepositoryHelper, - ObservationHelper observationHelper) { + ObservationHelper observationHelper, + SecureBaseUrlResolver secureBaseUrlResolver) { super( sessionUserService, userService, @@ -62,6 +64,7 @@ public ConsolidatedAPIServiceCECompatibleImpl( mockDataService, observationRegistry, cacheableRepositoryHelper, - observationHelper); + observationHelper, + secureBaseUrlResolver); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/UserServiceCECompatibleImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/UserServiceCECompatibleImpl.java index d8b838acafe1..d0ec7ebce04e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/UserServiceCECompatibleImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce_compatible/UserServiceCECompatibleImpl.java @@ -1,6 +1,7 @@ package com.appsmith.server.services.ce_compatible; import com.appsmith.server.configurations.CommonConfig; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.helpers.UserServiceHelper; import com.appsmith.server.helpers.UserUtils; import com.appsmith.server.instanceconfigs.helpers.InstanceVariablesHelper; @@ -39,7 +40,8 @@ public UserServiceCECompatibleImpl( RateLimitService rateLimitService, PACConfigurationService pacConfigurationService, UserServiceHelper userServiceHelper, - InstanceVariablesHelper instanceVariablesHelper) { + InstanceVariablesHelper instanceVariablesHelper, + SecureBaseUrlResolver secureBaseUrlResolver) { super( validator, repository, @@ -57,6 +59,7 @@ public UserServiceCECompatibleImpl( rateLimitService, pacConfigurationService, userServiceHelper, - instanceVariablesHelper); + instanceVariablesHelper, + secureBaseUrlResolver); } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCEImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCEImplTest.java new file mode 100644 index 000000000000..a0e89b27fd70 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/SecureBaseUrlResolverCEImplTest.java @@ -0,0 +1,235 @@ +package com.appsmith.server.helpers.ce; + +import com.appsmith.server.exceptions.AppsmithException; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Unit tests for {@link SecureBaseUrlResolverCEImpl}. + * + *

These tests pin the fail-closed semantics added for + * GHSA-j9gf-vw2f-9hrw: + * when {@code APPSMITH_BASE_URL} is unset, the resolver must NOT trust the request-supplied + * {@code Origin} value as the host of token-bearing email links. It must signal "no trusted + * URL available" via {@link reactor.core.publisher.Mono#empty()} so that callers in unauthenticated + * flows return generic success without dispatching email (preserving anti-enumeration), while + * callers in authenticated flows can convert the empty into an explicit configuration error. + * + *

The opt-in {@code APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS} compatibility flag exists + * solely to give operators a migration window. When enabled, it restores the legacy behavior + * of trusting the caller-supplied value — but it does NOT weaken the strict-mode check that + * applies once {@code APPSMITH_BASE_URL} is configured. + */ +class SecureBaseUrlResolverCEImplTest { + + private SecureBaseUrlResolverCEImpl newResolver(String configuredBaseUrl, boolean allowInsecureFallback) + throws Exception { + SecureBaseUrlResolverCEImpl resolver = new SecureBaseUrlResolverCEImpl(); + Field baseUrlField = SecureBaseUrlResolverCEImpl.class.getDeclaredField("appsmithBaseUrl"); + baseUrlField.setAccessible(true); + baseUrlField.set(resolver, configuredBaseUrl == null ? "" : configuredBaseUrl); + Field flagField = SecureBaseUrlResolverCEImpl.class.getDeclaredField("allowInsecureOriginBasedLinks"); + flagField.setAccessible(true); + flagField.set(resolver, allowInsecureFallback); + return resolver; + } + + /** + * GHSA-j9gf-vw2f-9hrw — the central regression test. When the trusted base URL is unset and + * the insecure compatibility flag is off (the new default), the resolver must return an + * empty signal — NOT the caller-supplied value. + */ + @Test + void resolveSecureBaseUrl_whenAppsmithBaseUrlUnsetAndCompatFlagOff_returnsEmpty() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("https://attacker.example")) + .verifyComplete(); + } + + /** + * Migration path: when an operator opts into the insecure flag during a transition window, + * the legacy behavior is restored — the caller-supplied value is returned. The WARN log is + * the operational signal that this is happening; we do not assert on the log here, but the + * production code MUST emit it (manual code review). + */ + @Test + void resolveSecureBaseUrl_whenAppsmithBaseUrlUnsetAndCompatFlagOn_returnsProvidedValue() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("", true); + + StepVerifier.create(resolver.resolveSecureBaseUrl("https://attacker.example")) + .expectNext("https://attacker.example") + .verifyComplete(); + } + + @Test + void resolveSecureBaseUrl_whenAppsmithBaseUrlSetAndOriginMatches_returnsConfiguredUrl() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("https://appsmith.example")) + .expectNext("https://appsmith.example") + .verifyComplete(); + } + + /** + * Regression on the protection added in PR #41426 (GHSA-7hf5-mc28-xmcv): once configured, + * the trusted base URL must not be impersonated. + */ + @Test + void resolveSecureBaseUrl_whenAppsmithBaseUrlSetAndOriginMismatches_errors() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("https://attacker.example")) + .verifyError(AppsmithException.class); + } + + /** + * Defense-in-depth: the insecure-compat flag is intended for the unset-config case only. + * It must NOT be a backdoor that weakens strict-mode validation when APPSMITH_BASE_URL is + * configured. + */ + @Test + void resolveSecureBaseUrl_whenAppsmithBaseUrlSetAndOriginMismatches_compatFlagDoesNotWeakenStrictMode() + throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", true); + + StepVerifier.create(resolver.resolveSecureBaseUrl("https://attacker.example")) + .verifyError(AppsmithException.class); + } + + /** + * Empty Origin from the request, unset config, default fail-closed: empty signal. + * Sanity-check that the resolver does not blow up on null/empty caller-supplied values. + */ + @Test + void resolveSecureBaseUrl_whenProvidedBaseUrlIsNullOrBlank_andCompatFlagOff_returnsEmpty() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl(null)).verifyComplete(); + StepVerifier.create(resolver.resolveSecureBaseUrl("")).verifyComplete(); + } + + @Test + void resolveSecureBaseUrl_constructed_isNotNull() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false); + assertNotNull(resolver); + } + + // region URL-origin normalisation — comparison must follow RFC 6454 (scheme + host + effective port), + // not raw string equality. Without this, real-world deployments hit spurious mismatches whenever the + // configured value and the inbound `Origin` header differ on insignificant syntax (trailing slash, + // default-port elision, host case). All accepted matches below resolve to the SAME origin per the + // RFC; all rejected ones genuinely differ in scheme, host, or non-default port. + + @Test + void resolveSecureBaseUrl_whenConfiguredHasTrailingSlash_andOriginDoesNot_match() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("http://localhost/", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("http://localhost")) + .expectNext("http://localhost/") + .verifyComplete(); + } + + @Test + void resolveSecureBaseUrl_whenOriginHasTrailingSlash_andConfiguredDoesNot_match() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("http://localhost", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("http://localhost/")) + .expectNext("http://localhost") + .verifyComplete(); + } + + @Test + void resolveSecureBaseUrl_whenOriginHasDefaultHttpPort_andConfiguredOmitsIt_match() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("http://localhost", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("http://localhost:80")) + .expectNext("http://localhost") + .verifyComplete(); + } + + @Test + void resolveSecureBaseUrl_whenOriginHasDefaultHttpsPort_andConfiguredOmitsIt_match() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("https://appsmith.example:443")) + .expectNext("https://appsmith.example") + .verifyComplete(); + } + + @Test + void resolveSecureBaseUrl_whenHostCasingDiffers_match() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://Appsmith.Example", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("https://appsmith.example")) + .expectNext("https://Appsmith.Example") + .verifyComplete(); + } + + @Test + void resolveSecureBaseUrl_whenSchemesDiffer_errors() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("http://appsmith.example")) + .verifyError(AppsmithException.class); + } + + @Test + void resolveSecureBaseUrl_whenNonDefaultPortsDiffer_errors() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("http://localhost:8080", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("http://localhost:9090")) + .verifyError(AppsmithException.class); + } + + @Test + void resolveSecureBaseUrl_whenOriginIsMalformed_errors() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("not a url")).verifyError(AppsmithException.class); + } + + @Test + void resolveSecureBaseUrl_whenAttackerUsesUserinfoTrick_errors() throws Exception { + // Tricks like https://appsmith.example@evil.com must NOT be accepted as the same origin + // as https://appsmith.example. URI parsing places appsmith.example in userinfo and evil.com + // as the host — so the host comparison rejects. + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false); + + StepVerifier.create(resolver.resolveSecureBaseUrl("https://appsmith.example@evil.example")) + .verifyError(AppsmithException.class); + } + + // endregion + + // region isBaseUrlConfigurationHealthy — instance-config health signal driving the admin + // warning banner. The signal answers "can this instance generate token-bearing email links + // without depending on a request-time hint?". CE semantics: true iff APPSMITH_BASE_URL is + // set. The insecure-flag fallback intentionally does NOT mark the instance as healthy — + // operators who opted into the deprecated escape hatch should still see the warning so the + // deprecation pressure is preserved. + + @Test + void isBaseUrlConfigurationHealthy_returnsTrueWhenBaseUrlSet() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("https://appsmith.example", false); + + StepVerifier.create(resolver.isBaseUrlConfigurationHealthy()) + .expectNext(true) + .verifyComplete(); + } + + @Test + void isBaseUrlConfigurationHealthy_returnsFalseWhenBaseUrlBlank() throws Exception { + SecureBaseUrlResolverCEImpl resolver = newResolver("", false); + + StepVerifier.create(resolver.isBaseUrlConfigurationHealthy()) + .expectNext(false) + .verifyComplete(); + } + + // endregion +} diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java index 8c3c7eeddec2..75096632618f 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java @@ -34,6 +34,7 @@ import com.appsmith.server.dtos.UserProfileDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.SecureBaseUrlResolver; import com.appsmith.server.jslibs.base.CustomJSLibService; import com.appsmith.server.newactions.base.NewActionService; import com.appsmith.server.newpages.base.NewPageService; @@ -50,6 +51,7 @@ import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.UserService; import com.appsmith.server.themes.base.ThemeService; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -103,6 +105,23 @@ public class ConsolidatedAPIServiceImplTest { @MockBean ProductAlertService mockProductAlertService; + /** + * GHSA-j9gf-vw2f-9hrw — the consolidated-api now calls + * {@link SecureBaseUrlResolver#isBaseUrlConfigurationHealthy()} alongside the org-config + * fetch. Mock it here so the test class doesn't escape into the real EE resolver chain + * (which transitively pulls in FeatureFlagService → OrganizationService and would NPE + * since OrganizationService is itself a @MockBean returning null for unstubbed methods). + */ + @MockBean + SecureBaseUrlResolver mockSecureBaseUrlResolver; + + @BeforeEach + void stubSecureBaseUrlResolver() { + // Default to healthy=true. Existing tests don't care about the admin-banner state; + // dedicated tests for the banner signal stub their own override on this mock. + when(mockSecureBaseUrlResolver.isBaseUrlConfigurationHealthy()).thenReturn(Mono.just(true)); + } + @SpyBean NewPageService spyNewPageService; diff --git a/app/server/appsmith-server/src/test/resources/application-test.properties b/app/server/appsmith-server/src/test/resources/application-test.properties index 878951df3000..324efe64371f 100644 --- a/app/server/appsmith-server/src/test/resources/application-test.properties +++ b/app/server/appsmith-server/src/test/resources/application-test.properties @@ -2,3 +2,10 @@ de.flapdoodle.mongodb.embedded.version=5.0.5 logging.level.root=error appsmith.git.root = /dev/shm/git-storage + +# GHSA-j9gf-vw2f-9hrw: enable the insecure-compatibility flag in the integration test +# environment so the legacy Origin-based behaviour is preserved across the existing +# Workspace / Fork / UserService / Theme integration suites. The fail-closed semantics +# of the resolver are pinned directly by SecureBaseUrlResolverCEImplTest unit tests; +# production deployments default this flag to false (secure). +APPSMITH_ALLOW_INSECURE_ORIGIN_BASED_LINKS=true diff --git a/scripts/deploy_preview.sh b/scripts/deploy_preview.sh index 8c9eafea96fd..a91fa8c78cf8 100755 --- a/scripts/deploy_preview.sh +++ b/scripts/deploy_preview.sh @@ -115,6 +115,7 @@ helm upgrade -i "$CHARTNAME" "appsmith-ee/$HELMCHART" -n "$NAMESPACE" --create-n --set applicationConfig.APPSMITH_BETTERBUGS_API_KEY="$APPSMITH_BETTERBUGS_API_KEY" \ --set applicationConfig.APPSMITH_PYLON_APP_ID="$APPSMITH_PYLON_APP_ID" \ --set applicationConfig.IN_DOCKER="$IN_DOCKER" \ + --set applicationConfig.APPSMITH_BASE_URL="https://$DOMAINNAME" \ --set applicationConfig.APPSMITH_CUSTOMER_PORTAL_URL="https://release-customer.appsmith.com" \ --set affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].key=instance_name \ --set affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator=In \ diff --git a/scripts/local_testing.sh b/scripts/local_testing.sh index b8fd6295ca68..fad1f8740e4a 100755 --- a/scripts/local_testing.sh +++ b/scripts/local_testing.sh @@ -121,4 +121,4 @@ docker build -t appsmith/appsmith-local-$edition:$tag \ pretty_print "Docker image build successful. Triggering run now ..." (docker stop appsmith || true) && (docker rm appsmith || true) -docker run -d --name appsmith -p 80:80 -v "$PWD/stacks:/appsmith-stacks" appsmith/appsmith-local-$edition:$tag && sleep 15 && pretty_print "Local instance is up! Open Appsmith at http://localhost! " +docker run -d --name appsmith -p 80:80 -v "$PWD/stacks:/appsmith-stacks" -e APPSMITH_BASE_URL=http://localhost appsmith/appsmith-local-$edition:$tag && sleep 15 && pretty_print "Local instance is up! Open Appsmith at http://localhost! "