Skip to content

Commit

Permalink
feat: embedded dashboard core (apache#17530)
Browse files Browse the repository at this point in the history
* feat(dashboard): embedded dashboard UI configuration (apache#17175) (apache#17450)

* setup embedded provider

* update ui configuration

* fix test

* feat: Guest token (for embedded dashboard auth) (apache#17517)

* generate an embed token

* improve existing tests

* add some auth setup, and rename token

* fix the stuff for compatibility with external request loaders

* docs, standard jwt claims, tweaks

* black

* lint

* tests, and safer token decoding

* linting

* type annotation

* prettier

* add feature flag

* quiet pylint

* apparently typing is a problem again

* Make guest role name configurable

* fake being a non-anonymous user

* just one log entry

* customizable algo

* lint

* lint again

* 403 works now!

* get guest token from header instead of cookie

* Revert "403 works now!"

This reverts commit df2f49a.

* fix tests

* Revert "Revert "403 works now!""

This reverts commit 883dff3.

* rename method

* correct import

* feat: entry for embedded dashboard (apache#17529)

* create entry for embedded dashboard in webpack

* add cookies

* lint

* token message handshake

* guestTokenHeaderName

* use setupClient instead of calling configure

* rename the webpack chunk

* simplified handshake

* embedded entrypoint: render a proper app

* make the embedded page accept anonymous connections

* format

* lint

* fix test
# Conflicts:
#	superset-frontend/src/embedded/index.tsx
#	superset/views/core.py

* lint

* Update superset-frontend/src/embedded/index.tsx

Co-authored-by: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com>

* comment out origins checks

* move embedded for core to dashboard

* pylint

* isort

Co-authored-by: David Aaron Suddjian <aasuddjian@gmail.com>
Co-authored-by: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com>

* feat: Authorizing guest access to embedded dashboards (apache#17757)

* helper methods and dashboard access

* guest token dashboard authz

* adjust csrf exempt list

* eums don't work that way

* Remove unnecessary import

* move row level security tests to their own file

* a bit of refactoring

* add guest token security tests

* refactor tests

* clean imports

* variable names can be too long apparently

* missing argument to get_user_roles

* don't redefine builtins

* remove unused imports

* fix test import

* default to global user when getting roles

* missing import

* mock it

* test get_user_roles

* infer g.user for ease of tests

* remove redundant check

* tests for guest user security manager fns

* use algo to get rid of warning messages

* tweaking access checks

* fix guest token security tests

* missing imports

* more tests

* more testing and also some small refactoring

* move validation out of parsing

* fix dashboard access check again

* add more test

Co-authored-by: Lily Kuang <lily@preset.io>

* feat: Row Level Security rules for guest tokens (apache#17836)

* helper methods and dashboard access

* guest token dashboard authz

* adjust csrf exempt list

* eums don't work that way

* Remove unnecessary import

* move row level security tests to their own file

* a bit of refactoring

* add guest token security tests

* refactor tests

* clean imports

* variable names can be too long apparently

* missing argument to get_user_roles

* don't redefine builtins

* remove unused imports

* fix test import

* default to global user when getting roles

* missing import

* mock it

* test get_user_roles

* infer g.user for ease of tests

* remove redundant check

* tests for guest user security manager fns

* use algo to get rid of warning messages

* tweaking access checks

* fix guest token security tests

* missing imports

* more tests

* more testing and also some small refactoring

* move validation out of parsing

* fix dashboard access check again

* rls rules for guest tokens

* test guest token rls rules

* more flexible rls rules

* lint

* fix tests

* fix test

* defaults

* fix some tests

* fix some tests

* lint

Co-authored-by: Lily Kuang <lily@preset.io>

* SupersetClient guest token test

* Apply suggestions from code review

Co-authored-by: Lily Kuang <lily@preset.io>

Co-authored-by: Lily Kuang <lily@preset.io>
  • Loading branch information
2 people authored and bwang221 committed Feb 10, 2022
1 parent ebace6b commit 6333165
Show file tree
Hide file tree
Showing 31 changed files with 1,466 additions and 325 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import SupersetClientClass from './SupersetClientClass';
import { SupersetClientInterface } from './types';

// this is local to this file, don't expose it
let singletonClient: SupersetClientClass | undefined;

function getInstance(): SupersetClientClass {
Expand All @@ -39,7 +40,6 @@ const SupersetClient: SupersetClientInterface = {
reset: () => {
singletonClient = undefined;
},
getInstance,
delete: request => getInstance().delete(request),
get: request => getInstance().get(request),
init: force => getInstance().init(force),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export default class SupersetClientClass {

csrfPromise?: CsrfPromise;

guestToken?: string;

guestTokenHeaderName: string;

fetchRetryOptions?: FetchRetryOptions;

baseUrl: string;
Expand All @@ -64,6 +68,8 @@ export default class SupersetClientClass {
timeout,
credentials = undefined,
csrfToken = undefined,
guestToken = undefined,
guestTokenHeaderName = 'X-GuestToken',
}: ClientConfig = {}) {
const url = new URL(
host || protocol
Expand All @@ -81,6 +87,8 @@ export default class SupersetClientClass {
this.timeout = timeout;
this.credentials = credentials;
this.csrfToken = csrfToken;
this.guestToken = guestToken;
this.guestTokenHeaderName = guestTokenHeaderName;
this.fetchRetryOptions = {
...DEFAULT_FETCH_RETRY_OPTIONS,
...fetchRetryOptions,
Expand All @@ -89,6 +97,9 @@ export default class SupersetClientClass {
this.headers = { ...this.headers, 'X-CSRFToken': this.csrfToken };
this.csrfPromise = Promise.resolve(this.csrfToken);
}
if (guestToken) {
this.headers[guestTokenHeaderName] = guestToken;
}
}

async init(force = false): CsrfPromise {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export interface ClientConfig {
protocol?: Protocol;
credentials?: Credentials;
csrfToken?: CsrfToken;
guestToken?: string;
guestTokenHeaderName?: string;
fetchRetryOptions?: FetchRetryOptions;
headers?: Headers;
mode?: Mode;
Expand All @@ -149,7 +151,6 @@ export interface SupersetClientInterface
| 'reAuthenticate'
> {
configure: (config?: ClientConfig) => SupersetClientClass;
getInstance: (maybeClient?: SupersetClientClass) => SupersetClientClass;
reset: () => void;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export enum FeatureFlag {
DASHBOARD_CROSS_FILTERS = 'DASHBOARD_CROSS_FILTERS',
DASHBOARD_NATIVE_FILTERS_SET = 'DASHBOARD_NATIVE_FILTERS_SET',
DASHBOARD_FILTERS_EXPERIMENTAL = 'DASHBOARD_FILTERS_EXPERIMENTAL',
EMBEDDED_SUPERSET = 'EMBEDDED_SUPERSET',
ENABLE_FILTER_BOX_MIGRATION = 'ENABLE_FILTER_BOX_MIGRATION',
VERSIONED_EXPORT = 'VERSIONED_EXPORT',
GLOBAL_ASYNC_QUERIES = 'GLOBAL_ASYNC_QUERIES',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,26 @@ describe('SupersetClientClass', () => {
);
});

it('uses a guest token when provided', async () => {
expect.assertions(1);

const client = new SupersetClientClass({
protocol,
host,
guestToken: 'abc123',
guestTokenHeaderName: 'guestTokenHeader',
});

await client.init();
await client.get({ url: mockGetUrl });
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi;
expect(fetchRequest.headers).toEqual(
expect.objectContaining({
guestTokenHeader: 'abc123',
}),
);
});

describe('.get()', () => {
it('makes a request using url or endpoint', async () => {
expect.assertions(2);
Expand Down
57 changes: 57 additions & 0 deletions superset-frontend/src/components/UiConfigContext/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { createContext, useContext, useState } from 'react';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';

interface UiConfigType {
hideTitle: boolean;
hideTab: boolean;
hideNav: boolean;
hideChartControls: boolean;
}
interface EmbeddedUiConfigProviderProps {
children: JSX.Element;
}

export const UiConfigContext = createContext<UiConfigType>({
hideTitle: false,
hideTab: false,
hideNav: false,
hideChartControls: false,
});

export const useUiConfig = () => useContext(UiConfigContext);

export const EmbeddedUiConfigProvider: React.FC<EmbeddedUiConfigProviderProps> =
({ children }) => {
const config = getUrlParam(URL_PARAMS.uiConfig);
const [embeddedConfig] = useState({
hideTitle: (config & 1) !== 0,
hideTab: (config & 2) !== 0,
hideNav: (config & 4) !== 0,
hideChartControls: (config & 8) !== 0,
});

return (
<UiConfigContext.Provider value={embeddedConfig}>
{children}
</UiConfigContext.Provider>
);
};
4 changes: 4 additions & 0 deletions superset-frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const URL_PARAMS = {
name: 'standalone',
type: 'number',
},
uiConfig: {
name: 'uiConfig',
type: 'number',
},
preselectFilters: {
name: 'preselect_filters',
type: 'object',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar';
import Loading from 'src/components/Loading';
import { Global } from '@emotion/react';
import { useUiConfig } from 'src/components/UiConfigContext';
import { shouldFocusTabs, getRootLevelTabsComponent } from './utils';
import DashboardContainer from './DashboardContainer';
import { useNativeFilters } from './state';
Expand Down Expand Up @@ -199,6 +200,8 @@ const StyledDashboardContent = styled.div<{

const DashboardBuilder: FC<DashboardBuilderProps> = () => {
const dispatch = useDispatch();
const uiConfig = useUiConfig();

const dashboardLayout = useSelector<RootState, DashboardLayout>(
state => state.dashboardLayout.present,
);
Expand Down Expand Up @@ -243,7 +246,9 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
const standaloneMode = getUrlParam(URL_PARAMS.standalone);
const isReport = standaloneMode === DashboardStandaloneMode.REPORT;
const hideDashboardHeader =
standaloneMode === DashboardStandaloneMode.HIDE_NAV_AND_TITLE || isReport;
uiConfig.hideTitle ||
standaloneMode === DashboardStandaloneMode.HIDE_NAV_AND_TITLE ||
isReport;

const barTopOffset =
(hideDashboardHeader ? 0 : HEADER_HEIGHT) +
Expand Down Expand Up @@ -288,7 +293,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
<div>
{!hideDashboardHeader && <DashboardHeader />}
{dropIndicatorProps && <div {...dropIndicatorProps} />}
{!isReport && topLevelTabs && (
{!isReport && topLevelTabs && !uiConfig.hideNav && (
<WithPopoverMenu
shouldFocus={shouldFocusTabs}
menuItems={[
Expand Down Expand Up @@ -321,6 +326,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
hideDashboardHeader,
isReport,
topLevelTabs,
uiConfig.hideNav,
],
);

Expand Down
59 changes: 32 additions & 27 deletions superset-frontend/src/dashboard/components/SliceHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
import React, { FC, useMemo } from 'react';
import { styled, t } from '@superset-ui/core';
import { useUiConfig } from 'src/components/UiConfigContext';
import { Tooltip } from 'src/components/Tooltip';
import { useDispatch, useSelector } from 'react-redux';
import EditableTitle from 'src/components/EditableTitle';
Expand All @@ -44,7 +45,6 @@ type SliceHeaderProps = SliceHeaderControlsProps & {

const annotationsLoading = t('Annotation layers are still loading.');
const annotationsError = t('One ore more annotation layers failed loading.');

const CrossFilterIcon = styled(Icons.CursorTarget)`
cursor: pointer;
color: ${({ theme }) => theme.colors.primary.base};
Expand Down Expand Up @@ -84,6 +84,7 @@ const SliceHeader: FC<SliceHeaderProps> = ({
formData,
}) => {
const dispatch = useDispatch();
const uiConfig = useUiConfig();
// TODO: change to indicator field after it will be implemented
const crossFilterValue = useSelector<RootState, any>(
state => state.dataMask[slice?.slice_id]?.filterState?.value,
Expand Down Expand Up @@ -157,32 +158,36 @@ const SliceHeader: FC<SliceHeaderProps> = ({
/>
</Tooltip>
)}
<FiltersBadge chartId={slice.slice_id} />
<SliceHeaderControls
slice={slice}
isCached={isCached}
isExpanded={isExpanded}
cachedDttm={cachedDttm}
updatedDttm={updatedDttm}
toggleExpandSlice={toggleExpandSlice}
forceRefresh={forceRefresh}
logExploreChart={logExploreChart}
exploreUrl={exploreUrl}
exportCSV={exportCSV}
exportFullCSV={exportFullCSV}
supersetCanExplore={supersetCanExplore}
supersetCanShare={supersetCanShare}
supersetCanCSV={supersetCanCSV}
sliceCanEdit={sliceCanEdit}
componentId={componentId}
dashboardId={dashboardId}
addSuccessToast={addSuccessToast}
addDangerToast={addDangerToast}
handleToggleFullSize={handleToggleFullSize}
isFullSize={isFullSize}
chartStatus={chartStatus}
formData={formData}
/>
{!uiConfig.hideChartControls && (
<FiltersBadge chartId={slice.slice_id} />
)}
{!uiConfig.hideChartControls && (
<SliceHeaderControls
slice={slice}
isCached={isCached}
isExpanded={isExpanded}
cachedDttm={cachedDttm}
updatedDttm={updatedDttm}
toggleExpandSlice={toggleExpandSlice}
forceRefresh={forceRefresh}
logExploreChart={logExploreChart}
exploreUrl={exploreUrl}
exportCSV={exportCSV}
exportFullCSV={exportFullCSV}
supersetCanExplore={supersetCanExplore}
supersetCanShare={supersetCanShare}
supersetCanCSV={supersetCanCSV}
sliceCanEdit={sliceCanEdit}
componentId={componentId}
dashboardId={dashboardId}
addSuccessToast={addSuccessToast}
addDangerToast={addDangerToast}
handleToggleFullSize={handleToggleFullSize}
isFullSize={isFullSize}
chartStatus={chartStatus}
formData={formData}
/>
)}
</>
)}
</div>
Expand Down

0 comments on commit 6333165

Please sign in to comment.