diff --git a/client/app/components/ApplicationArea/routeWithApiKeySession.jsx b/client/app/components/ApplicationArea/routeWithApiKeySession.jsx index 66f04b79c..771a14c94 100644 --- a/client/app/components/ApplicationArea/routeWithApiKeySession.jsx +++ b/client/app/components/ApplicationArea/routeWithApiKeySession.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useContext } from "react"; import PropTypes from "prop-types"; import { ErrorBoundaryContext } from "@redash/viz/lib/components/ErrorBoundary"; -import { Auth } from "@/services/auth"; +import { Auth, clientConfig } from "@/services/auth"; // This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object // that contains: @@ -33,7 +33,7 @@ function ApiKeySessionWrapper({ apiKey, currentRoute, renderChildren }) { }; }, [apiKey]); - if (!isAuthenticated) { + if (!isAuthenticated || clientConfig.disablePublicUrls) { return null; } diff --git a/client/app/components/EditVisualizationButton/QueryControlDropdown.jsx b/client/app/components/EditVisualizationButton/QueryControlDropdown.jsx index c12a609b3..3997726e0 100644 --- a/client/app/components/EditVisualizationButton/QueryControlDropdown.jsx +++ b/client/app/components/EditVisualizationButton/QueryControlDropdown.jsx @@ -3,6 +3,7 @@ import PropTypes from "prop-types"; import Dropdown from "antd/lib/dropdown"; import Menu from "antd/lib/menu"; import Button from "antd/lib/button"; +import { clientConfig } from "@/services/auth"; import PlusCircleFilledIcon from "@ant-design/icons/PlusCircleFilled"; import ShareAltOutlinedIcon from "@ant-design/icons/ShareAltOutlined"; @@ -22,7 +23,7 @@ export default function QueryControlDropdown(props) { )} - {!props.query.isNew() && ( + {!clientConfig.disablePublicUrls && !props.query.isNew() && ( props.showEmbedDialog(props.query, props.selectedTab)} data-test="ShowEmbedDialogButton"> Embed Elsewhere diff --git a/client/app/pages/dashboards/components/DashboardHeader.jsx b/client/app/pages/dashboards/components/DashboardHeader.jsx index 6222fce54..3191314fc 100644 --- a/client/app/pages/dashboards/components/DashboardHeader.jsx +++ b/client/app/pages/dashboards/components/DashboardHeader.jsx @@ -178,7 +178,8 @@ function DashboardControl({ dashboardOptions }) { const showPublishButton = dashboard.is_draft; const showRefreshButton = true; const showFullscreenButton = !dashboard.is_draft; - const showShareButton = dashboard.publicAccessEnabled || (canEditDashboard && !dashboard.is_draft); + const canShareDashboard = canEditDashboard && !dashboard.is_draft; + const showShareButton = !clientConfig.disablePublicUrls && (dashboard.publicAccessEnabled || canShareDashboard); const showMoreOptionsButton = canEditDashboard; return (
diff --git a/client/app/pages/queries/components/QueryPageHeader.jsx b/client/app/pages/queries/components/QueryPageHeader.jsx index b925e94e1..00310dec5 100644 --- a/client/app/pages/queries/components/QueryPageHeader.jsx +++ b/client/app/pages/queries/components/QueryPageHeader.jsx @@ -123,7 +123,7 @@ export default function QueryPageHeader({ }, { showAPIKey: { - isAvailable: !queryFlags.isNew, + isAvailable: !clientConfig.disablePublicUrls && !queryFlags.isNew, title: "Show API Key", onClick: openApiKeyDialog, }, @@ -199,7 +199,7 @@ export default function QueryPageHeader({ {!queryFlags.isNew && ( - diff --git a/client/cypress/integration/dashboard/sharing_spec.js b/client/cypress/integration/dashboard/sharing_spec.js index 3ddcab8a1..5e73b54e5 100644 --- a/client/cypress/integration/dashboard/sharing_spec.js +++ b/client/cypress/integration/dashboard/sharing_spec.js @@ -9,6 +9,37 @@ describe("Dashboard Sharing", () => { this.dashboardId = id; this.dashboardUrl = `/dashboards/${id}`; }); + cy.updateOrgSettings({ disable_public_urls: false }); + }); + + it("is unavailable when public urls feature is disabled", function() { + const queryData = { + query: "select 1", + }; + + const position = { autoHeight: false, sizeY: 6 }; + createQueryAndAddWidget(this.dashboardId, queryData, { position }) + .then(() => { + cy.visit(this.dashboardUrl); + return shareDashboard(); + }) + .then(secretAddress => { + // disable the feature + cy.updateOrgSettings({ disable_public_urls: true }); + + // check the feature is disabled + cy.visit(this.dashboardUrl); + cy.getByTestId("DashboardMoreButton").should("exist"); + cy.getByTestId("OpenShareForm").should("not.exist"); + + cy.logout(); + cy.visit(secretAddress); + cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.getByTestId("TableVisualization").should("not.exist"); + + cy.login(); + cy.updateOrgSettings({ disable_public_urls: false }); + }); }); it("is possible if all queries are safe", function() { diff --git a/client/cypress/integration/embed/share_embed_spec.js b/client/cypress/integration/embed/share_embed_spec.js index 520482979..fea7f0ecb 100644 --- a/client/cypress/integration/embed/share_embed_spec.js +++ b/client/cypress/integration/embed/share_embed_spec.js @@ -1,6 +1,43 @@ describe("Embedded Queries", () => { beforeEach(() => { cy.login(); + cy.updateOrgSettings({ disable_public_urls: false }); + }); + + it("is unavailable when public urls feature is disabled", () => { + cy.createQuery({ query: "select name from users order by name" }).then(query => { + cy.visit(`/queries/${query.id}/source`); + cy.getByTestId("ExecuteButton").click(); + cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist"); + cy.clickThrough(` + QueryControlDropdownButton + ShowEmbedDialogButton + `); + cy.getByTestId("EmbedIframe") + .invoke("text") + .then(embedUrl => { + // disable the feature + cy.updateOrgSettings({ disable_public_urls: true }); + + // check the feature is disabled + cy.visit(`/queries/${query.id}/source`); + cy.getByTestId("QueryPageHeaderMoreButton").click(); + cy.get(".ant-dropdown-menu-item") + .should("exist") + .should("not.contain", "Show API Key"); + cy.getByTestId("QueryControlDropdownButton").click(); + cy.get(".ant-dropdown-menu-item").should("exist"); + cy.getByTestId("ShowEmbedDialogButton").should("not.exist"); + + cy.logout(); + cy.visit(embedUrl); + cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.getByTestId("TableVisualization").should("not.exist"); + + cy.login(); + cy.updateOrgSettings({ disable_public_urls: false }); + }); + }); }); it("can be shared without parameters", () => { diff --git a/client/cypress/support/redash-api/index.js b/client/cypress/support/redash-api/index.js index 1292f9e1a..e9bc0ace6 100644 --- a/client/cypress/support/redash-api/index.js +++ b/client/cypress/support/redash-api/index.js @@ -165,3 +165,7 @@ Cypress.Commands.add("addDestinationSubscription", (alertId, destinationName) => return body; }); }); + +Cypress.Commands.add("updateOrgSettings", settings => { + return post({ url: "api/settings/organization", body: settings }).then(({ body }) => body); +}); diff --git a/redash/handlers/authentication.py b/redash/handlers/authentication.py index a4f1d2f87..bee4487e6 100644 --- a/redash/handlers/authentication.py +++ b/redash/handlers/authentication.py @@ -288,6 +288,7 @@ def client_config(): "hidePlotlyModeBar": current_org.get_setting( "hide_plotly_mode_bar" ), + "disablePublicUrls": current_org.get_setting("disable_public_urls"), "allowCustomJSVisualizations": settings.FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS, "autoPublishNamedQueries": settings.FEATURE_AUTO_PUBLISH_NAMED_QUERIES, "extendedAlertOptions": settings.FEATURE_EXTENDED_ALERT_OPTIONS, diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py index cfc710930..5aace124d 100644 --- a/redash/handlers/dashboards.py +++ b/redash/handlers/dashboards.py @@ -268,6 +268,9 @@ class PublicDashboardResource(BaseResource): :param token: An API key for a public dashboard. :>json array widgets: An array of arrays of :ref:`public widgets `, corresponding to the rows and columns the widgets are displayed in """ + if self.current_org.get_setting("disable_public_urls"): + abort(400, message="Public URLs are disabled.") + if not isinstance(self.current_user, models.ApiUser): api_key = get_object_or_404(models.ApiKey.get_by_api_key, token) dashboard = api_key.object diff --git a/redash/settings/organization.py b/redash/settings/organization.py index 782802a46..0181a7026 100644 --- a/redash/settings/organization.py +++ b/redash/settings/organization.py @@ -43,8 +43,9 @@ FEATURE_SHOW_PERMISSIONS_CONTROL = parse_boolean( SEND_EMAIL_ON_FAILED_SCHEDULED_QUERIES = parse_boolean( os.environ.get("REDASH_SEND_EMAIL_ON_FAILED_SCHEDULED_QUERIES", "false") ) -HIDE_PLOTLY_MODE_BAR = parse_boolean( - os.environ.get("HIDE_PLOTLY_MODE_BAR", "false") +HIDE_PLOTLY_MODE_BAR = parse_boolean(os.environ.get("HIDE_PLOTLY_MODE_BAR", "false")) +DISABLE_PUBLIC_URLS = parse_boolean( + os.environ.get("REDASH_DISABLE_PUBLIC_URLS", "false") ) settings = { @@ -69,4 +70,5 @@ settings = { "feature_show_permissions_control": FEATURE_SHOW_PERMISSIONS_CONTROL, "send_email_on_failed_scheduled_queries": SEND_EMAIL_ON_FAILED_SCHEDULED_QUERIES, "hide_plotly_mode_bar": HIDE_PLOTLY_MODE_BAR, + "disable_public_urls": DISABLE_PUBLIC_URLS, }