You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by su...@apache.org on 2022/01/26 00:42:44 UTC

[superset] branch master updated: feat: embedded dashboard core (#17530)

This is an automated email from the ASF dual-hosted git repository.

suddjian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4ad5ad0  feat: embedded dashboard core (#17530)
4ad5ad0 is described below

commit 4ad5ad045a9adb506d14b2c02fdbefc564d25bdb
Author: David Aaron Suddjian <18...@users.noreply.github.com>
AuthorDate: Tue Jan 25 16:41:32 2022 -0800

    feat: embedded dashboard core (#17530)
    
    * feat(dashboard): embedded dashboard UI configuration (#17175) (#17450)
    
    * setup embedded provider
    
    * update ui configuration
    
    * fix test
    
    * feat: Guest token (for embedded dashboard auth) (#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 df2f49a6d4267b3cccccd66549d54e25bae8e0b6.
    
    * fix tests
    
    * Revert "Revert "403 works now!""
    
    This reverts commit 883dff38f16537e41f0eb5d699845263c96be5cb.
    
    * rename method
    
    * correct import
    
    * feat: entry for embedded dashboard (#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 <18...@users.noreply.github.com>
    
    * comment out origins checks
    
    * move embedded for core to dashboard
    
    * pylint
    
    * isort
    
    Co-authored-by: David Aaron Suddjian <aa...@gmail.com>
    Co-authored-by: David Aaron Suddjian <18...@users.noreply.github.com>
    
    * feat: Authorizing guest access to embedded dashboards (#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 <li...@preset.io>
    
    * feat: Row Level Security rules for guest tokens (#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 <li...@preset.io>
    
    * SupersetClient guest token test
    
    * Apply suggestions from code review
    
    Co-authored-by: Lily Kuang <li...@preset.io>
    
    Co-authored-by: Lily Kuang <li...@preset.io>
---
 .../src/connection/SupersetClient.ts               |   2 +-
 .../src/connection/SupersetClientClass.ts          |  11 +
 .../superset-ui-core/src/connection/types.ts       |   3 +-
 .../superset-ui-core/src/utils/featureFlags.ts     |   1 +
 .../test/connection/SupersetClientClass.test.ts    |  20 ++
 .../src/components/UiConfigContext/index.tsx       |  57 ++++
 superset-frontend/src/constants.ts                 |   4 +
 .../DashboardBuilder/DashboardBuilder.tsx          |  10 +-
 .../src/dashboard/components/SliceHeader/index.tsx |  59 ++--
 superset-frontend/src/embedded/index.tsx           | 117 ++++++++
 superset-frontend/src/preamble.ts                  |   4 +-
 superset-frontend/src/setup/setupClient.ts         |  13 +-
 superset-frontend/src/views/App.tsx                |  43 +--
 .../src/views/RootContextProviders.tsx             |  55 ++++
 superset-frontend/src/views/components/Menu.tsx    |   4 +-
 superset-frontend/webpack.config.js                |   1 +
 superset/common/request_contexed_based.py          |  16 +-
 superset/config.py                                 |  14 +-
 superset/connectors/sqla/models.py                 |  20 +-
 superset/dashboards/filters.py                     |  24 +-
 superset/security/api.py                           |  94 +++++-
 superset/security/guest_token.py                   |  87 ++++++
 superset/security/manager.py                       | 205 +++++++++++--
 superset/templates/superset/spa.html               |   2 +-
 superset/views/base.py                             |  11 +-
 superset/views/core.py                             |   5 +-
 superset/views/dashboard/views.py                  |  44 ++-
 tests/integration_tests/security/api_tests.py      |  49 +++-
 .../security/guest_token_security_tests.py         | 210 ++++++++++++++
 .../security/row_level_security_tests.py           | 316 +++++++++++++++++++++
 tests/integration_tests/security_tests.py          | 290 +++++++------------
 31 files changed, 1466 insertions(+), 325 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClient.ts b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClient.ts
index b582025..0f4c123 100644
--- a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClient.ts
+++ b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClient.ts
@@ -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 {
@@ -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),
diff --git a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts
index ef52134..39d5022 100644
--- a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts
+++ b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts
@@ -40,6 +40,10 @@ export default class SupersetClientClass {
 
   csrfPromise?: CsrfPromise;
 
+  guestToken?: string;
+
+  guestTokenHeaderName: string;
+
   fetchRetryOptions?: FetchRetryOptions;
 
   baseUrl: string;
@@ -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
@@ -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,
@@ -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 {
diff --git a/superset-frontend/packages/superset-ui-core/src/connection/types.ts b/superset-frontend/packages/superset-ui-core/src/connection/types.ts
index 3f02f1c..b8df5a9 100644
--- a/superset-frontend/packages/superset-ui-core/src/connection/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/connection/types.ts
@@ -130,6 +130,8 @@ export interface ClientConfig {
   protocol?: Protocol;
   credentials?: Credentials;
   csrfToken?: CsrfToken;
+  guestToken?: string;
+  guestTokenHeaderName?: string;
   fetchRetryOptions?: FetchRetryOptions;
   headers?: Headers;
   mode?: Mode;
@@ -149,7 +151,6 @@ export interface SupersetClientInterface
     | 'reAuthenticate'
   > {
   configure: (config?: ClientConfig) => SupersetClientClass;
-  getInstance: (maybeClient?: SupersetClientClass) => SupersetClientClass;
   reset: () => void;
 }
 
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
index a55dcfd..cab4f72 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
@@ -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',
diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
index 4b2307c..4efd4f4 100644
--- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
@@ -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);
diff --git a/superset-frontend/src/components/UiConfigContext/index.tsx b/superset-frontend/src/components/UiConfigContext/index.tsx
new file mode 100644
index 0000000..4b77e42
--- /dev/null
+++ b/superset-frontend/src/components/UiConfigContext/index.tsx
@@ -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>
+    );
+  };
diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts
index a57951a..996cd7d 100644
--- a/superset-frontend/src/constants.ts
+++ b/superset-frontend/src/constants.ts
@@ -31,6 +31,10 @@ export const URL_PARAMS = {
     name: 'standalone',
     type: 'number',
   },
+  uiConfig: {
+    name: 'uiConfig',
+    type: 'number',
+  },
   preselectFilters: {
     name: 'preselect_filters',
     type: 'object',
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 5e7f71b..bd6e663 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -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';
@@ -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,
   );
@@ -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) +
@@ -288,7 +293,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
       <div>
         {!hideDashboardHeader && <DashboardHeader />}
         {dropIndicatorProps && <div {...dropIndicatorProps} />}
-        {!isReport && topLevelTabs && (
+        {!isReport && topLevelTabs && !uiConfig.hideNav && (
           <WithPopoverMenu
             shouldFocus={shouldFocusTabs}
             menuItems={[
@@ -321,6 +326,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
       hideDashboardHeader,
       isReport,
       topLevelTabs,
+      uiConfig.hideNav,
     ],
   );
 
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index 9b42c81..a92fcc4 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -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';
@@ -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};
@@ -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,
@@ -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>
diff --git a/superset-frontend/src/embedded/index.tsx b/superset-frontend/src/embedded/index.tsx
new file mode 100644
index 0000000..11a686f
--- /dev/null
+++ b/superset-frontend/src/embedded/index.tsx
@@ -0,0 +1,117 @@
+/**
+ * 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, { lazy, Suspense } from 'react';
+import ReactDOM from 'react-dom';
+import { BrowserRouter as Router, Route } from 'react-router-dom';
+import { bootstrapData } from 'src/preamble';
+import setupClient from 'src/setup/setupClient';
+import { RootContextProviders } from 'src/views/RootContextProviders';
+import ErrorBoundary from 'src/components/ErrorBoundary';
+import Loading from 'src/components/Loading';
+
+const LazyDashboardPage = lazy(
+  () =>
+    import(
+      /* webpackChunkName: "DashboardPage" */ 'src/dashboard/containers/DashboardPage'
+    ),
+);
+
+const EmbeddedApp = () => (
+  <Router>
+    <Route path="/dashboard/:idOrSlug/embedded">
+      <Suspense fallback={<Loading />}>
+        <RootContextProviders>
+          <ErrorBoundary>
+            <LazyDashboardPage />
+          </ErrorBoundary>
+        </RootContextProviders>
+      </Suspense>
+    </Route>
+  </Router>
+);
+
+const appMountPoint = document.getElementById('app')!;
+
+const MESSAGE_TYPE = '__embedded_comms__';
+
+if (!window.parent) {
+  appMountPoint.innerHTML =
+    'This page is intended to be embedded in an iframe, but no window.parent was found.';
+}
+
+// if the page is embedded in an origin that hasn't
+// been authorized by the curator, we forbid access entirely.
+// todo: check the referrer on the route serving this page instead
+// const ALLOW_ORIGINS = ['http://127.0.0.1:9001', 'http://localhost:9001'];
+// const parentOrigin = new URL(document.referrer).origin;
+// if (!ALLOW_ORIGINS.includes(parentOrigin)) {
+//   throw new Error(
+//     `[superset] iframe parent ${parentOrigin} is not in the list of allowed origins`,
+//   );
+// }
+
+async function start(guestToken: string) {
+  // the preamble configures a client, but we need to configure a new one
+  // now that we have the guest token
+  setupClient({
+    guestToken,
+    guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
+  });
+  ReactDOM.render(<EmbeddedApp />, appMountPoint);
+}
+
+function validateMessageEvent(event: MessageEvent) {
+  if (
+    event.data?.type === 'webpackClose' ||
+    event.data?.source === '@devtools-page'
+  ) {
+    // sometimes devtools use the messaging api and we want to ignore those
+    throw new Error("Sir, this is a Wendy's");
+  }
+
+  // if (!ALLOW_ORIGINS.includes(event.origin)) {
+  //   throw new Error('Message origin is not in the allowed list');
+  // }
+
+  if (typeof event.data !== 'object' || event.data.type !== MESSAGE_TYPE) {
+    throw new Error(`Message type does not match type used for embedded comms`);
+  }
+}
+
+window.addEventListener('message', function (event) {
+  try {
+    validateMessageEvent(event);
+  } catch (err) {
+    console.info('[superset] ignoring message', err, event);
+    return;
+  }
+
+  console.info('[superset] received message', event);
+  const hostAppPort = event.ports?.[0];
+  if (hostAppPort) {
+    hostAppPort.onmessage = function receiveMessage(event) {
+      console.info('[superset] received message event', event.data);
+      if (event.data.guestToken) {
+        start(event.data.guestToken);
+      }
+    };
+  }
+});
+
+console.info('[superset] embed page is ready to receive messages');
diff --git a/superset-frontend/src/preamble.ts b/superset-frontend/src/preamble.ts
index da2fac3..5380fe2 100644
--- a/superset-frontend/src/preamble.ts
+++ b/superset-frontend/src/preamble.ts
@@ -30,11 +30,11 @@ if (process.env.WEBPACK_MODE === 'development') {
   setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false });
 }
 
-let bootstrapData: any;
+// eslint-disable-next-line import/no-mutable-exports
+export let bootstrapData: any;
 // Configure translation
 if (typeof window !== 'undefined') {
   const root = document.getElementById('app');
-
   bootstrapData = root
     ? JSON.parse(root.getAttribute('data-bootstrap') || '{}')
     : {};
diff --git a/superset-frontend/src/setup/setupClient.ts b/superset-frontend/src/setup/setupClient.ts
index 11f3f6a..8802ae4 100644
--- a/superset-frontend/src/setup/setupClient.ts
+++ b/superset-frontend/src/setup/setupClient.ts
@@ -16,22 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { SupersetClient, logging } from '@superset-ui/core';
+import { SupersetClient, logging, ClientConfig } from '@superset-ui/core';
 import parseCookie from 'src/utils/parseCookie';
 
-export default function setupClient() {
+function getDefaultConfiguration(): ClientConfig {
   const csrfNode = document.querySelector<HTMLInputElement>('#csrf_token');
   const csrfToken = csrfNode?.value;
 
   // when using flask-jwt-extended csrf is set in cookies
   const cookieCSRFToken = parseCookie().csrf_access_token || '';
 
-  SupersetClient.configure({
+  return {
     protocol: ['http:', 'https:'].includes(window?.location?.protocol)
       ? (window?.location?.protocol as 'http:' | 'https:')
       : undefined,
     host: (window.location && window.location.host) || '',
     csrfToken: csrfToken || cookieCSRFToken,
+  };
+}
+
+export default function setupClient(customConfig: Partial<ClientConfig> = {}) {
+  SupersetClient.configure({
+    ...getDefaultConfiguration(),
+    ...customConfig,
   })
     .init()
     .catch(error => {
diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx
index 7bbc40e..4e193bb 100644
--- a/superset-frontend/src/views/App.tsx
+++ b/superset-frontend/src/views/App.tsx
@@ -18,41 +18,31 @@
  */
 import React, { Suspense, useEffect } from 'react';
 import { hot } from 'react-hot-loader/root';
-import { Provider as ReduxProvider } from 'react-redux';
 import {
   BrowserRouter as Router,
   Switch,
   Route,
   useLocation,
 } from 'react-router-dom';
-import { DndProvider } from 'react-dnd';
-import { HTML5Backend } from 'react-dnd-html5-backend';
-import { QueryParamProvider } from 'use-query-params';
 import { initFeatureFlags } from 'src/featureFlags';
-import { ThemeProvider } from '@superset-ui/core';
-import { DynamicPluginProvider } from 'src/components/DynamicPlugins';
 import ErrorBoundary from 'src/components/ErrorBoundary';
 import Loading from 'src/components/Loading';
 import Menu from 'src/views/components/Menu';
-import FlashProvider from 'src/components/FlashProvider';
-import { theme } from 'src/preamble';
+import { bootstrapData } from 'src/preamble';
 import ToastContainer from 'src/components/MessageToasts/ToastContainer';
 import setupApp from 'src/setup/setupApp';
 import { routes, isFrontendRoute } from 'src/views/routes';
 import { Logger } from 'src/logger/LogUtils';
-import { store } from './store';
+import { RootContextProviders } from './RootContextProviders';
 
 setupApp();
 
-const container = document.getElementById('app');
-const bootstrap = JSON.parse(container?.getAttribute('data-bootstrap') ?? '{}');
-const user = { ...bootstrap.user };
-const menu = { ...bootstrap.common.menu_data };
-const common = { ...bootstrap.common };
+const user = { ...bootstrapData.user };
+const menu = { ...bootstrapData.common.menu_data };
 let lastLocationPathname: string;
-initFeatureFlags(bootstrap.common.feature_flags);
+initFeatureFlags(bootstrapData.common.feature_flags);
 
-const RootContextProviders: React.FC = ({ children }) => {
+const LocationPathnameLogger = () => {
   const location = useLocation();
   useEffect(() => {
     // reset performance logger timer start point to avoid soft navigation
@@ -62,29 +52,12 @@ const RootContextProviders: React.FC = ({ children }) => {
     }
     lastLocationPathname = location.pathname;
   }, [location.pathname]);
-
-  return (
-    <ThemeProvider theme={theme}>
-      <ReduxProvider store={store}>
-        <DndProvider backend={HTML5Backend}>
-          <FlashProvider messages={common.flash_messages}>
-            <DynamicPluginProvider>
-              <QueryParamProvider
-                ReactRouterRoute={Route}
-                stringifyOptions={{ encode: false }}
-              >
-                {children}
-              </QueryParamProvider>
-            </DynamicPluginProvider>
-          </FlashProvider>
-        </DndProvider>
-      </ReduxProvider>
-    </ThemeProvider>
-  );
+  return <></>;
 };
 
 const App = () => (
   <Router>
+    <LocationPathnameLogger />
     <RootContextProviders>
       <Menu data={menu} isFrontendRoute={isFrontendRoute} />
       <Switch>
diff --git a/superset-frontend/src/views/RootContextProviders.tsx b/superset-frontend/src/views/RootContextProviders.tsx
new file mode 100644
index 0000000..f40f228
--- /dev/null
+++ b/superset-frontend/src/views/RootContextProviders.tsx
@@ -0,0 +1,55 @@
+/**
+ * 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 from 'react';
+import { Route } from 'react-router-dom';
+import { ThemeProvider } from '@superset-ui/core';
+import { Provider as ReduxProvider } from 'react-redux';
+import { QueryParamProvider } from 'use-query-params';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
+
+import { store } from './store';
+import FlashProvider from '../components/FlashProvider';
+import { bootstrapData, theme } from '../preamble';
+import { EmbeddedUiConfigProvider } from '../components/UiConfigContext';
+import { DynamicPluginProvider } from '../components/DynamicPlugins';
+
+const common = { ...bootstrapData.common };
+
+export const RootContextProviders: React.FC = ({ children }) => (
+  <ThemeProvider theme={theme}>
+    <ReduxProvider store={store}>
+      <DndProvider backend={HTML5Backend}>
+        <FlashProvider messages={common.flash_messages}>
+          <EmbeddedUiConfigProvider>
+            <DynamicPluginProvider>
+              <QueryParamProvider
+                ReactRouterRoute={Route}
+                stringifyOptions={{ encode: false }}
+              >
+                {children}
+              </QueryParamProvider>
+            </DynamicPluginProvider>
+          </EmbeddedUiConfigProvider>
+        </FlashProvider>
+      </DndProvider>
+    </ReduxProvider>
+  </ThemeProvider>
+);
diff --git a/superset-frontend/src/views/components/Menu.tsx b/superset-frontend/src/views/components/Menu.tsx
index fd998b3..a29671c 100644
--- a/superset-frontend/src/views/components/Menu.tsx
+++ b/superset-frontend/src/views/components/Menu.tsx
@@ -31,6 +31,7 @@ import {
 import { Tooltip } from 'src/components/Tooltip';
 import { Link } from 'react-router-dom';
 import Icons from 'src/components/Icons';
+import { useUiConfig } from 'src/components/UiConfigContext';
 import { URL_PARAMS } from 'src/constants';
 import RightMenu from './MenuRight';
 import { Languages } from './LanguagePicker';
@@ -182,6 +183,7 @@ export function Menu({
 }: MenuProps) {
   const [showMenu, setMenu] = useState<MenuMode>('horizontal');
   const screens = useBreakpoint();
+  const uiConig = useUiConfig();
 
   useEffect(() => {
     function handleResize() {
@@ -196,7 +198,7 @@ export function Menu({
   }, []);
 
   const standalone = getUrlParam(URL_PARAMS.standalone);
-  if (standalone) return <></>;
+  if (standalone || uiConig.hideNav) return <></>;
 
   const renderSubMenu = ({
     label,
diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js
index 17547da..55f0122 100644
--- a/superset-frontend/webpack.config.js
+++ b/superset-frontend/webpack.config.js
@@ -208,6 +208,7 @@ const config = {
     theme: path.join(APP_DIR, '/src/theme.ts'),
     menu: addPreamble('src/views/menu.tsx'),
     spa: addPreamble('/src/views/index.tsx'),
+    embedded: addPreamble('/src/embedded/index.tsx'),
     addSlice: addPreamble('/src/addSlice/index.tsx'),
     explore: addPreamble('/src/explore/index.jsx'),
     sqllab: addPreamble('/src/SqlLab/index.tsx'),
diff --git a/superset/common/request_contexed_based.py b/superset/common/request_contexed_based.py
index 0b06a0c..5d8405e 100644
--- a/superset/common/request_contexed_based.py
+++ b/superset/common/request_contexed_based.py
@@ -16,24 +16,10 @@
 # under the License.
 from __future__ import annotations
 
-from typing import List, TYPE_CHECKING
-
-from flask import g
-
 from superset import conf, security_manager
 
-if TYPE_CHECKING:
-    from flask_appbuilder.security.sqla.models import Role
-
-
-def get_user_roles() -> List[Role]:
-    if g.user.is_anonymous:
-        public_role = conf.get("AUTH_ROLE_PUBLIC")
-        return [security_manager.get_public_role()] if public_role else []
-    return g.user.roles
-
 
 def is_user_admin() -> bool:
-    user_roles = [role.name.lower() for role in get_user_roles()]
+    user_roles = [role.name.lower() for role in security_manager.get_user_roles()]
     admin_role = conf.get("AUTH_ROLE_ADMIN").lower()
     return admin_role in user_roles
diff --git a/superset/config.py b/superset/config.py
index 16d9d66..228a9f2 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -204,7 +204,11 @@ QUERY_SEARCH_LIMIT = 1000
 WTF_CSRF_ENABLED = True
 
 # Add endpoints that need to be exempt from CSRF protection
-WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log", "superset.charts.data.api.data"]
+WTF_CSRF_EXEMPT_LIST = [
+    "superset.views.core.log",
+    "superset.views.core.explore_json",
+    "superset.charts.data.api.data",
+]
 
 # Whether to run the web server in debug mode or not
 DEBUG = os.environ.get("FLASK_ENV") == "development"
@@ -406,6 +410,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
     # a custom security config could potentially give access to setting filters on
     # tables that users do not have access to.
     "ROW_LEVEL_SECURITY": True,
+    "EMBEDDED_SUPERSET": False,
     # Enables Alerts and reports new implementation
     "ALERT_REPORTS": False,
     # Enable experimental feature to search for other dashboards
@@ -1305,6 +1310,13 @@ GLOBAL_ASYNC_QUERIES_POLLING_DELAY = int(
 )
 GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = "ws://127.0.0.1:8080/"
 
+# Embedded config options
+GUEST_ROLE_NAME = "Public"
+GUEST_TOKEN_JWT_SECRET = "test-guest-secret-change-me"
+GUEST_TOKEN_JWT_ALGO = "HS256"
+GUEST_TOKEN_HEADER_NAME = "X-GuestToken"
+GUEST_TOKEN_JWT_EXP_SECONDS = 300  # 5 minutes
+
 # A SQL dataset health check. Note if enabled it is strongly advised that the callable
 # be memoized to aid with performance, i.e.,
 #
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 2251693..311889b 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -963,14 +963,28 @@ class SqlaTable(Model, BaseDatasource):  # pylint: disable=too-many-public-metho
         :returns: A list of SQL clauses to be ANDed together.
         :rtype: List[str]
         """
-        filters_grouped: Dict[Union[int, str], List[str]] = defaultdict(list)
+        all_filters: List[TextClause] = []
+        filter_groups: Dict[Union[int, str], List[TextClause]] = defaultdict(list)
         try:
             for filter_ in security_manager.get_rls_filters(self):
                 clause = self.text(
                     f"({template_processor.process_template(filter_.clause)})"
                 )
-                filters_grouped[filter_.group_key or filter_.id].append(clause)
-            return [or_(*clauses) for clauses in filters_grouped.values()]
+                if filter_.group_key:
+                    filter_groups[filter_.group_key].append(clause)
+                else:
+                    all_filters.append(clause)
+
+            if is_feature_enabled("EMBEDDED_SUPERSET"):
+                for rule in security_manager.get_guest_rls_filters(self):
+                    clause = self.text(
+                        f"({template_processor.process_template(rule['clause'])})"
+                    )
+                    all_filters.append(clause)
+
+            grouped_filters = [or_(*clauses) for clauses in filter_groups.values()]
+            all_filters.extend(grouped_filters)
+            return all_filters
         except TemplateError as ex:
             raise QueryObjectValidationError(
                 _("Error in jinja expression in RLS filters: %(msg)s", msg=ex.message,)
diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py
index 9658478..e398af9 100644
--- a/superset/dashboards/filters.py
+++ b/superset/dashboards/filters.py
@@ -16,6 +16,7 @@
 # under the License.
 from typing import Any, Optional
 
+from flask import g
 from flask_appbuilder.security.sqla.models import Role
 from flask_babel import lazy_gettext as _
 from sqlalchemy import and_, or_
@@ -25,7 +26,8 @@ from superset import db, is_feature_enabled, security_manager
 from superset.models.core import FavStar
 from superset.models.dashboard import Dashboard
 from superset.models.slice import Slice
-from superset.views.base import BaseFilter, get_user_roles, is_user_admin
+from superset.security.guest_token import GuestTokenResourceType, GuestUser
+from superset.views.base import BaseFilter, is_user_admin
 from superset.views.base_api import BaseFavoriteFilter
 
 
@@ -112,7 +114,7 @@ class DashboardAccessFilter(BaseFilter):  # pylint: disable=too-few-public-metho
             )
         )
 
-        dashboard_rbac_or_filters = []
+        feature_flagged_filters = []
         if is_feature_enabled("DASHBOARD_RBAC"):
             roles_based_query = (
                 db.session.query(Dashboard.id)
@@ -121,19 +123,31 @@ class DashboardAccessFilter(BaseFilter):  # pylint: disable=too-few-public-metho
                     and_(
                         Dashboard.published.is_(True),
                         dashboard_has_roles,
-                        Role.id.in_([x.id for x in get_user_roles()]),
+                        Role.id.in_([x.id for x in security_manager.get_user_roles()]),
                     ),
                 )
             )
 
-            dashboard_rbac_or_filters.append(Dashboard.id.in_(roles_based_query))
+            feature_flagged_filters.append(Dashboard.id.in_(roles_based_query))
+
+        if is_feature_enabled("EMBEDDED_SUPERSET") and security_manager.is_guest_user(
+            g.user
+        ):
+            guest_user: GuestUser = g.user
+            embedded_dashboard_ids = [
+                r["id"]
+                for r in guest_user.resources
+                if r["type"] == GuestTokenResourceType.DASHBOARD.value
+            ]
+            if len(embedded_dashboard_ids) != 0:
+                feature_flagged_filters.append(Dashboard.id.in_(embedded_dashboard_ids))
 
         query = query.filter(
             or_(
                 Dashboard.id.in_(owner_ids_query),
                 Dashboard.id.in_(datasource_perm_query),
                 Dashboard.id.in_(users_favorite_dash_query),
-                *dashboard_rbac_or_filters,
+                *feature_flagged_filters,
             )
         )
 
diff --git a/superset/security/api.py b/superset/security/api.py
index 5aa51f8..b919e29 100644
--- a/superset/security/api.py
+++ b/superset/security/api.py
@@ -15,18 +15,64 @@
 # specific language governing permissions and limitations
 # under the License.
 import logging
+from typing import Any, Dict
 
-from flask import Response
+from flask import request, Response
 from flask_appbuilder import expose
 from flask_appbuilder.api import BaseApi, safe
 from flask_appbuilder.security.decorators import permission_name, protect
 from flask_wtf.csrf import generate_csrf
+from marshmallow import EXCLUDE, fields, post_load, Schema, ValidationError
+from marshmallow_enum import EnumField
 
 from superset.extensions import event_logger
+from superset.security.guest_token import GuestTokenResourceType
 
 logger = logging.getLogger(__name__)
 
 
+class PermissiveSchema(Schema):
+    """
+    A marshmallow schema that ignores unexpected fields, instead of throwing an error.
+    """
+
+    class Meta:  # pylint: disable=too-few-public-methods
+        unknown = EXCLUDE
+
+
+class UserSchema(PermissiveSchema):
+    username = fields.String()
+    first_name = fields.String()
+    last_name = fields.String()
+
+
+class ResourceSchema(PermissiveSchema):
+    type = EnumField(GuestTokenResourceType, by_value=True, required=True)
+    id = fields.String(required=True)
+
+    @post_load
+    def convert_enum_to_value(  # pylint: disable=no-self-use
+        self, data: Dict[str, Any], **kwargs: Any  # pylint: disable=unused-argument
+    ) -> Dict[str, Any]:
+        # we don't care about the enum, we want the value inside
+        data["type"] = data["type"].value
+        return data
+
+
+class RlsRuleSchema(PermissiveSchema):
+    dataset = fields.Integer()
+    clause = fields.String(required=True)  # todo other options?
+
+
+class GuestTokenCreateSchema(PermissiveSchema):
+    user = fields.Nested(UserSchema)
+    resources = fields.List(fields.Nested(ResourceSchema), required=True)
+    rls = fields.List(fields.Nested(RlsRuleSchema), required=True)
+
+
+guest_token_create_schema = GuestTokenCreateSchema()
+
+
 class SecurityRestApi(BaseApi):
     resource_name = "security"
     allow_browser_login = True
@@ -60,3 +106,49 @@ class SecurityRestApi(BaseApi):
               $ref: '#/components/responses/500'
         """
         return self.response(200, result=generate_csrf())
+
+    @expose("/guest_token/", methods=["POST"])
+    @event_logger.log_this
+    @protect()
+    @safe
+    @permission_name("grant_guest_token")
+    def guest_token(self) -> Response:
+        """Response
+        Returns a guest token that can be used for auth in embedded Superset
+        ---
+        post:
+          description: >-
+            Fetches a guest token
+          requestBody:
+            description: Parameters for the guest token
+            required: true
+            content:
+              application/json:
+                schema: GuestTokenCreateSchema
+          responses:
+            200:
+              description: Result contains the guest token
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                        token:
+                          type: string
+            401:
+              $ref: '#/components/responses/401'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            body = guest_token_create_schema.load(request.json)
+            # todo validate stuff:
+            # make sure the resource ids are valid
+            # make sure username doesn't reference an existing user
+            # check rls rules for validity?
+            token = self.appbuilder.sm.create_guest_access_token(
+                body["user"], body["resources"], body["rls"]
+            )
+            return self.response(200, token=token)
+        except ValidationError as error:
+            return self.response_400(message=error.messages)
diff --git a/superset/security/guest_token.py b/superset/security/guest_token.py
new file mode 100644
index 0000000..44b59c1
--- /dev/null
+++ b/superset/security/guest_token.py
@@ -0,0 +1,87 @@
+# 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.
+from enum import Enum
+from typing import List, Optional, TypedDict, Union
+
+from flask_appbuilder.security.sqla.models import Role
+from flask_login import AnonymousUserMixin
+
+
+class GuestTokenUser(TypedDict, total=False):
+    username: str
+    first_name: str
+    last_name: str
+
+
+class GuestTokenResourceType(Enum):
+    DASHBOARD = "dashboard"
+
+
+class GuestTokenResource(TypedDict):
+    type: GuestTokenResourceType
+    id: Union[str, int]
+
+
+GuestTokenResources = List[GuestTokenResource]
+
+
+class GuestTokenRlsRule(TypedDict):
+    dataset: Optional[str]
+    clause: str
+
+
+class GuestToken(TypedDict):
+    iat: float
+    exp: float
+    user: GuestTokenUser
+    resources: GuestTokenResources
+    rls_rules: List[GuestTokenRlsRule]
+
+
+class GuestUser(AnonymousUserMixin):
+    """
+    Used as the "anonymous" user in case of guest authentication (embedded)
+    """
+
+    is_guest_user = True
+
+    @property
+    def is_authenticated(self) -> bool:
+        """
+        This is set to true because guest users should be considered authenticated,
+        at least in most places. The treatment of this flag is kind of inconsistent.
+        """
+        return True
+
+    @property
+    def is_anonymous(self) -> bool:
+        """
+        This is set to false because lots of code assumes that
+        if user.is_anonymous, then role = Public
+        But guest users need to have their own role independent of Public.
+        """
+        return False
+
+    def __init__(self, token: GuestToken, roles: List[Role]):
+        user = token["user"]
+        self.guest_token = token
+        self.username = user.get("username", "guest_user")
+        self.first_name = user.get("first_name", "Guest")
+        self.last_name = user.get("last_name", "User")
+        self.roles = roles
+        self.resources = token["resources"]
+        self.rls = token.get("rls_rules", [])
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 9e45e88..5ca81b2 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -18,6 +18,7 @@
 """A set of constants and methods to manage permissions and security"""
 import logging
 import re
+import time
 from collections import defaultdict
 from typing import (
     Any,
@@ -32,7 +33,8 @@ from typing import (
     Union,
 )
 
-from flask import current_app, g
+import jwt
+from flask import current_app, Flask, g, Request
 from flask_appbuilder import Model
 from flask_appbuilder.models.sqla.interface import SQLAInterface
 from flask_appbuilder.security.sqla.manager import SecurityManager
@@ -51,7 +53,7 @@ from flask_appbuilder.security.views import (
     ViewMenuModelView,
 )
 from flask_appbuilder.widgets import ListWidget
-from flask_login import AnonymousUserMixin
+from flask_login import AnonymousUserMixin, LoginManager
 from sqlalchemy import and_, or_
 from sqlalchemy.engine.base import Connection
 from sqlalchemy.orm import Session
@@ -63,6 +65,14 @@ from superset.connectors.connector_registry import ConnectorRegistry
 from superset.constants import RouteMethod
 from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.exceptions import SupersetSecurityException
+from superset.security.guest_token import (
+    GuestToken,
+    GuestTokenResources,
+    GuestTokenResourceType,
+    GuestTokenRlsRule,
+    GuestTokenUser,
+    GuestUser,
+)
 from superset.utils.core import DatasourceName, RowLevelSecurityFilterType
 
 if TYPE_CHECKING:
@@ -172,6 +182,7 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         "can_approve",
         "can_update_role",
         "all_query_access",
+        "can_grant_guest_token",
     }
 
     READ_ONLY_PERMISSION = {
@@ -221,6 +232,17 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         "all_query_access",
     )
 
+    guest_user_cls = GuestUser
+
+    def create_login_manager(self, app: Flask) -> LoginManager:
+        # pylint: disable=import-outside-toplevel
+        from superset.extensions import feature_flag_manager
+
+        lm = super().create_login_manager(app)
+        if feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"):
+            lm.request_loader(self.get_guest_user_from_request)
+        return lm
+
     def get_schema_perm(  # pylint: disable=no-self-use
         self, database: Union["Database", str], schema: Optional[str] = None
     ) -> Optional[str]:
@@ -1047,11 +1069,16 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
 
             assert datasource
 
+            should_check_dashboard_access = (
+                feature_flag_manager.is_feature_enabled("DASHBOARD_RBAC")
+                or self.is_guest_user()
+            )
+
             if not (
                 self.can_access_schema(datasource)
                 or self.can_access("datasource_access", datasource.perm or "")
                 or (
-                    feature_flag_manager.is_feature_enabled("DASHBOARD_RBAC")
+                    should_check_dashboard_access
                     and self.can_access_based_on_dashboard(datasource)
                 )
             ):
@@ -1077,6 +1104,33 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
     def get_anonymous_user(self) -> User:  # pylint: disable=no-self-use
         return AnonymousUserMixin()
 
+    def get_user_roles(self, user: Optional[User] = None) -> List[Role]:
+        if not user:
+            user = g.user
+        if user.is_anonymous:
+            public_role = current_app.config.get("AUTH_ROLE_PUBLIC")
+            return [self.get_public_role()] if public_role else []
+        return user.roles
+
+    def get_guest_rls_filters(
+        self, dataset: "BaseDatasource"
+    ) -> List[GuestTokenRlsRule]:
+        """
+        Retrieves the row level security filters for the current user and the dataset,
+        if the user is authenticated with a guest token.
+        :param dataset: The dataset to check against
+        :return: A list of filters
+        """
+        guest_user = self.get_current_guest_user_if_guest()
+        if guest_user:
+            return [
+                rule
+                for rule in guest_user.rls
+                if not rule.get("dataset")
+                or str(rule.get("dataset")) == str(dataset.id)
+            ]
+        return []
+
     def get_rls_filters(self, table: "BaseDatasource") -> List[SqlaQuery]:
         """
         Retrieves the appropriate row level security filters for the current user and
@@ -1085,7 +1139,7 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         :param table: The table to check against
         :returns: A list of filters
         """
-        if hasattr(g, "user") and hasattr(g.user, "id"):
+        if hasattr(g, "user"):
             # pylint: disable=import-outside-toplevel
             from superset.connectors.sqla.models import (
                 RLSFilterRoles,
@@ -1093,11 +1147,7 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
                 RowLevelSecurityFilter,
             )
 
-            user_roles = (
-                self.get_session.query(assoc_user_role.c.role_id)
-                .filter(assoc_user_role.c.user_id == g.user.get_id())
-                .subquery()
-            )
+            user_roles = [role.id for role in self.get_user_roles()]
             regular_filter_roles = (
                 self.get_session.query(RLSFilterRoles.c.rls_filter_id)
                 .join(RowLevelSecurityFilter)
@@ -1175,10 +1225,11 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
                 )
             )
 
-    @staticmethod
-    def raise_for_dashboard_access(dashboard: "Dashboard") -> None:
+    def raise_for_dashboard_access(self, dashboard: "Dashboard") -> None:
         """
         Raise an exception if the user cannot access the dashboard.
+        This does not check for the required role/permission pairs,
+        it only concerns itself with entity relationships.
 
         :param dashboard: Dashboard the user wants access to
         :raises DashboardAccessDeniedError: If the user cannot access the resource
@@ -1186,23 +1237,27 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         # pylint: disable=import-outside-toplevel
         from superset import is_feature_enabled
         from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
-        from superset.views.base import get_user_roles, is_user_admin
+        from superset.views.base import is_user_admin
         from superset.views.utils import is_owner
 
-        has_rbac_access = True
-
-        if is_feature_enabled("DASHBOARD_RBAC"):
-            has_rbac_access = any(
-                dashboard_role.id in [user_role.id for user_role in get_user_roles()]
+        def has_rbac_access() -> bool:
+            return (not is_feature_enabled("DASHBOARD_RBAC")) or any(
+                dashboard_role.id
+                in [user_role.id for user_role in self.get_user_roles()]
                 for dashboard_role in dashboard.roles
             )
 
-        can_access = (
-            is_user_admin()
-            or is_owner(dashboard, g.user)
-            or (dashboard.published and has_rbac_access)
-            or (not dashboard.published and not dashboard.roles)
-        )
+        if self.is_guest_user():
+            can_access = self.has_guest_access(
+                GuestTokenResourceType.DASHBOARD, dashboard.id
+            )
+        else:
+            can_access = (
+                is_user_admin()
+                or is_owner(dashboard, g.user)
+                or (dashboard.published and has_rbac_access())
+                or (not dashboard.published and not dashboard.roles)
+            )
 
         if not can_access:
             raise DashboardAccessDeniedError()
@@ -1228,3 +1283,107 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
 
         exists = db.session.query(query.exists()).scalar()
         return exists
+
+    @staticmethod
+    def _get_current_epoch_time() -> float:
+        """ This is used so the tests can mock time """
+        return time.time()
+
+    def create_guest_access_token(
+        self,
+        user: GuestTokenUser,
+        resources: GuestTokenResources,
+        rls: List[GuestTokenRlsRule],
+    ) -> bytes:
+        secret = current_app.config["GUEST_TOKEN_JWT_SECRET"]
+        algo = current_app.config["GUEST_TOKEN_JWT_ALGO"]
+        exp_seconds = current_app.config["GUEST_TOKEN_JWT_EXP_SECONDS"]
+
+        # calculate expiration time
+        now = self._get_current_epoch_time()
+        exp = now + (exp_seconds * 1000)
+        claims = {
+            "user": user,
+            "resources": resources,
+            "rls_rules": rls,
+            # standard jwt claims:
+            "iat": now,  # issued at
+            "exp": exp,  # expiration time
+        }
+        token = jwt.encode(claims, secret, algorithm=algo)
+        return token
+
+    def get_guest_user_from_request(self, req: Request) -> Optional[GuestUser]:
+        """
+        If there is a guest token in the request (used for embedded),
+        parses the token and returns the guest user.
+        This is meant to be used as a request loader for the LoginManager.
+        The LoginManager will only call this if an active session cannot be found.
+
+        :return: A guest user object
+        """
+        raw_token = req.headers.get(current_app.config["GUEST_TOKEN_HEADER_NAME"])
+        if raw_token is None:
+            return None
+
+        try:
+            token = self.parse_jwt_guest_token(raw_token)
+            if token.get("user") is None:
+                raise ValueError("Guest token does not contain a user claim")
+            if token.get("resources") is None:
+                raise ValueError("Guest token does not contain a resources claim")
+            if token.get("rls_rules") is None:
+                raise ValueError("Guest token does not contain an rls_rules claim")
+        except Exception:  # pylint: disable=broad-except
+            # The login manager will handle sending 401s.
+            # We don't need to send a special error message.
+            logger.warning("Invalid guest token", exc_info=True)
+            return None
+        else:
+            return self.get_guest_user_from_token(cast(GuestToken, token))
+
+    def get_guest_user_from_token(self, token: GuestToken) -> GuestUser:
+        return self.guest_user_cls(
+            token=token, roles=[self.find_role(current_app.config["GUEST_ROLE_NAME"])],
+        )
+
+    @staticmethod
+    def parse_jwt_guest_token(raw_token: str) -> Dict[str, Any]:
+        """
+        Parses a guest token. Raises an error if the jwt fails standard claims checks.
+        :param raw_token: the token gotten from the request
+        :return: the same token that was passed in, tested but unchanged
+        """
+        secret = current_app.config["GUEST_TOKEN_JWT_SECRET"]
+        algo = current_app.config["GUEST_TOKEN_JWT_ALGO"]
+        return jwt.decode(raw_token, secret, algorithms=[algo])
+
+    @staticmethod
+    def is_guest_user(user: Optional[Any] = None) -> bool:
+        # pylint: disable=import-outside-toplevel
+        from superset import is_feature_enabled
+
+        if not is_feature_enabled("EMBEDDED_SUPERSET"):
+            return False
+        if not user:
+            user = g.user
+        return hasattr(user, "is_guest_user") and user.is_guest_user
+
+    def get_current_guest_user_if_guest(self) -> Optional[GuestUser]:
+
+        if self.is_guest_user():
+            return g.user
+        return None
+
+    def has_guest_access(
+        self, resource_type: GuestTokenResourceType, resource_id: Union[str, int]
+    ) -> bool:
+        user = self.get_current_guest_user_if_guest()
+        if not user:
+            return False
+
+        strid = str(resource_id)
+        for resource in user.resources:
+            if resource["type"] == resource_type.value and str(resource["id"]) == strid:
+                return True
+        return False
diff --git a/superset/templates/superset/spa.html b/superset/templates/superset/spa.html
index 1e38cb2..6a0312f 100644
--- a/superset/templates/superset/spa.html
+++ b/superset/templates/superset/spa.html
@@ -22,6 +22,6 @@
 {% endblock %}
 
 {% block tail_js %}
-  {{ js_bundle("spa") }}
+  {{ js_bundle(entry) }}
   {% include "tail_js_custom_extra.html" %}
 {% endblock %}
diff --git a/superset/views/base.py b/superset/views/base.py
index d3cb477..4244c66 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -38,7 +38,7 @@ from flask_appbuilder import BaseView, Model, ModelView
 from flask_appbuilder.actions import action
 from flask_appbuilder.forms import DynamicForm
 from flask_appbuilder.models.sqla.filters import BaseFilter
-from flask_appbuilder.security.sqla.models import Role, User
+from flask_appbuilder.security.sqla.models import User
 from flask_appbuilder.widgets import ListWidget
 from flask_babel import get_locale, gettext as __, lazy_gettext as _
 from flask_jwt_extended.exceptions import NoAuthorizationError
@@ -264,15 +264,8 @@ def create_table_permissions(table: models.SqlaTable) -> None:
         security_manager.add_permission_view_menu("schema_access", table.schema_perm)
 
 
-def get_user_roles() -> List[Role]:
-    if g.user.is_anonymous:
-        public_role = conf.get("AUTH_ROLE_PUBLIC")
-        return [security_manager.find_role(public_role)] if public_role else []
-    return g.user.roles
-
-
 def is_user_admin() -> bool:
-    user_roles = [role.name.lower() for role in list(get_user_roles())]
+    user_roles = [role.name.lower() for role in list(security_manager.get_user_roles())]
     return "admin" in user_roles
 
 
diff --git a/superset/views/core.py b/superset/views/core.py
index 8e115a9..84ef5eb 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -135,7 +135,6 @@ from superset.views.base import (
     data_payload_response,
     generate_download_headers,
     get_error_msg,
-    get_user_roles,
     handle_api_exception,
     json_error_response,
     json_errors_response,
@@ -1888,7 +1887,9 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
                 f"ERROR: cannot find dashboard {dashboard_id}", status=404
             )
 
-        edit_perm = is_owner(dash, g.user) or admin_role in get_user_roles()
+        edit_perm = (
+            is_owner(dash, g.user) or admin_role in security_manager.get_user_roles()
+        )
         if not edit_perm:
             username = g.user.username if hasattr(g.user, "username") else "user"
             return json_error_response(
diff --git a/superset/views/dashboard/views.py b/superset/views/dashboard/views.py
index 5e03d24..99782de 100644
--- a/superset/views/dashboard/views.py
+++ b/superset/views/dashboard/views.py
@@ -16,7 +16,7 @@
 # under the License.
 import json
 import re
-from typing import List, Union
+from typing import Callable, List, Union
 
 from flask import g, redirect, request, Response
 from flask_appbuilder import expose
@@ -24,8 +24,9 @@ from flask_appbuilder.actions import action
 from flask_appbuilder.models.sqla.interface import SQLAInterface
 from flask_appbuilder.security.decorators import has_access
 from flask_babel import gettext as __, lazy_gettext as _
+from flask_login import AnonymousUserMixin, LoginManager
 
-from superset import db, event_logger, is_feature_enabled
+from superset import db, event_logger, is_feature_enabled, security_manager
 from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod
 from superset.models.dashboard import Dashboard as DashboardModel
 from superset.typing import FlaskResponse
@@ -33,6 +34,7 @@ from superset.utils import core as utils
 from superset.views.base import (
     BaseSupersetView,
     check_ownership,
+    common_bootstrap_payload,
     DeleteMixin,
     generate_download_headers,
     SupersetModelView,
@@ -133,6 +135,44 @@ class Dashboard(BaseSupersetView):
         db.session.commit()
         return redirect(f"/superset/dashboard/{new_dashboard.id}/?edit=true")
 
+    @expose("/<dashboard_id_or_slug>/embedded")
+    @event_logger.log_this_with_extra_payload
+    def embedded(
+        self,
+        dashboard_id_or_slug: str,
+        add_extra_log_payload: Callable[..., None] = lambda **kwargs: None,
+    ) -> FlaskResponse:
+        """
+        Server side rendering for a dashboard
+        :param dashboard_id_or_slug: identifier for dashboard. used in the decorators
+        :param add_extra_log_payload: added by `log_this_with_manual_updates`, set a
+            default value to appease pylint
+        """
+        if not is_feature_enabled("EMBEDDED_SUPERSET"):
+            return Response(status=404)
+
+        # Log in as an anonymous user, just for this view.
+        # This view needs to be visible to all users,
+        # and building the page fails if g.user and/or ctx.user aren't present.
+        login_manager: LoginManager = security_manager.lm
+        login_manager.reload_user(AnonymousUserMixin())
+
+        add_extra_log_payload(
+            dashboard_id=dashboard_id_or_slug, dashboard_version="v2",
+        )
+
+        bootstrap_data = {
+            "common": common_bootstrap_payload(),
+        }
+
+        return self.render_template(
+            "superset/spa.html",
+            entry="embedded",
+            bootstrap_data=json.dumps(
+                bootstrap_data, default=utils.pessimistic_json_iso_dttm_ser
+            ),
+        )
+
 
 class DashboardModelViewAsync(DashboardModelView):  # pylint: disable=too-many-ancestors
     route_base = "/dashboardasync"
diff --git a/tests/integration_tests/security/api_tests.py b/tests/integration_tests/security/api_tests.py
index 6a81efa..d7b3659 100644
--- a/tests/integration_tests/security/api_tests.py
+++ b/tests/integration_tests/security/api_tests.py
@@ -15,22 +15,24 @@
 # specific language governing permissions and limitations
 # under the License.
 # isort:skip_file
-"""Unit tests for Superset"""
+"""Tests for security api methods"""
 import json
 
+import jwt
+
 from tests.integration_tests.base_tests import SupersetTestCase
 from flask_wtf.csrf import generate_csrf
 
 
-class TestSecurityApi(SupersetTestCase):
+class TestSecurityCsrfApi(SupersetTestCase):
     resource_name = "security"
 
     def _assert_get_csrf_token(self):
         uri = f"api/v1/{self.resource_name}/csrf_token/"
         response = self.client.get(uri)
-        assert response.status_code == 200
+        self.assert200(response)
         data = json.loads(response.data.decode("utf-8"))
-        assert data["result"] == generate_csrf()
+        self.assertEqual(generate_csrf(), data["result"])
 
     def test_get_csrf_token(self):
         """
@@ -53,4 +55,41 @@ class TestSecurityApi(SupersetTestCase):
         self.logout()
         uri = f"api/v1/{self.resource_name}/csrf_token/"
         response = self.client.get(uri)
-        self.assertEqual(response.status_code, 401)
+        self.assert401(response)
+
+
+class TestSecurityGuestTokenApi(SupersetTestCase):
+    uri = f"api/v1/security/guest_token/"
+
+    def test_post_guest_token_unauthenticated(self):
+        """
+        Security API: Cannot create a guest token without authentication
+        """
+        self.logout()
+        response = self.client.post(self.uri)
+        self.assert401(response)
+
+    def test_post_guest_token_unauthorized(self):
+        """
+        Security API: Cannot create a guest token without authorization
+        """
+        self.login(username="gamma")
+        response = self.client.post(self.uri)
+        self.assert403(response)
+
+    def test_post_guest_token_authorized(self):
+        self.login(username="admin")
+        user = {"username": "bob", "first_name": "Bob", "last_name": "Also Bob"}
+        resource = {"type": "dashboard", "id": "blah"}
+        rls_rule = {"dataset": 1, "clause": "1=1"}
+        params = {"user": user, "resources": [resource], "rls": [rls_rule]}
+
+        response = self.client.post(
+            self.uri, data=json.dumps(params), content_type="application/json"
+        )
+
+        self.assert200(response)
+        token = json.loads(response.data)["token"]
+        decoded_token = jwt.decode(token, self.app.config["GUEST_TOKEN_JWT_SECRET"])
+        self.assertEqual(user, decoded_token["user"])
+        self.assertEqual(resource, decoded_token["resources"][0])
diff --git a/tests/integration_tests/security/guest_token_security_tests.py b/tests/integration_tests/security/guest_token_security_tests.py
new file mode 100644
index 0000000..9ca3419
--- /dev/null
+++ b/tests/integration_tests/security/guest_token_security_tests.py
@@ -0,0 +1,210 @@
+# 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.
+"""Unit tests for Superset"""
+from unittest import mock
+
+import pytest
+from flask import g
+
+from superset import db, security_manager
+from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
+from superset.exceptions import SupersetSecurityException
+from superset.models.dashboard import Dashboard
+from superset.security.guest_token import GuestTokenResourceType
+from superset.sql_parse import Table
+from tests.integration_tests.base_tests import SupersetTestCase
+from tests.integration_tests.fixtures.birth_names_dashboard import (
+    load_birth_names_dashboard_with_slices,
+    load_birth_names_data,
+)
+
+
+@mock.patch.dict(
+    "superset.extensions.feature_flag_manager._feature_flags", EMBEDDED_SUPERSET=True,
+)
+class TestGuestUserSecurity(SupersetTestCase):
+    # This test doesn't use a dashboard fixture, the next test does.
+    # That way tests are faster.
+
+    resource_id = 42
+
+    def authorized_guest(self):
+        return security_manager.get_guest_user_from_token(
+            {"user": {}, "resources": [{"type": "dashboard", "id": self.resource_id}]}
+        )
+
+    def test_is_guest_user__regular_user(self):
+        is_guest = security_manager.is_guest_user(security_manager.find_user("admin"))
+        self.assertFalse(is_guest)
+
+    def test_is_guest_user__anonymous(self):
+        is_guest = security_manager.is_guest_user(security_manager.get_anonymous_user())
+        self.assertFalse(is_guest)
+
+    def test_is_guest_user__guest_user(self):
+        is_guest = security_manager.is_guest_user(self.authorized_guest())
+        self.assertTrue(is_guest)
+
+    @mock.patch.dict(
+        "superset.extensions.feature_flag_manager._feature_flags",
+        EMBEDDED_SUPERSET=False,
+    )
+    def test_is_guest_user__flag_off(self):
+        is_guest = security_manager.is_guest_user(self.authorized_guest())
+        self.assertFalse(is_guest)
+
+    def test_get_guest_user__regular_user(self):
+        g.user = security_manager.find_user("admin")
+        guest_user = security_manager.get_current_guest_user_if_guest()
+        self.assertIsNone(guest_user)
+
+    def test_get_guest_user__anonymous_user(self):
+        g.user = security_manager.get_anonymous_user()
+        guest_user = security_manager.get_current_guest_user_if_guest()
+        self.assertIsNone(guest_user)
+
+    def test_get_guest_user__guest_user(self):
+        g.user = self.authorized_guest()
+        guest_user = security_manager.get_current_guest_user_if_guest()
+        self.assertEqual(guest_user, g.user)
+
+    def test_has_guest_access__regular_user(self):
+        g.user = security_manager.find_user("admin")
+        has_guest_access = security_manager.has_guest_access(
+            GuestTokenResourceType.DASHBOARD, self.resource_id
+        )
+        self.assertFalse(has_guest_access)
+
+    def test_has_guest_access__anonymous_user(self):
+        g.user = security_manager.get_anonymous_user()
+        has_guest_access = security_manager.has_guest_access(
+            GuestTokenResourceType.DASHBOARD, self.resource_id
+        )
+        self.assertFalse(has_guest_access)
+
+    def test_has_guest_access__authorized_guest_user(self):
+        g.user = self.authorized_guest()
+        has_guest_access = security_manager.has_guest_access(
+            GuestTokenResourceType.DASHBOARD, self.resource_id
+        )
+        self.assertTrue(has_guest_access)
+
+    def test_has_guest_access__authorized_guest_user__non_zero_resource_index(self):
+        guest = self.authorized_guest()
+        guest.resources = [
+            {"type": "dashboard", "id": self.resource_id - 1}
+        ] + guest.resources
+        g.user = guest
+
+        has_guest_access = security_manager.has_guest_access(
+            GuestTokenResourceType.DASHBOARD, self.resource_id
+        )
+        self.assertTrue(has_guest_access)
+
+    def test_has_guest_access__unauthorized_guest_user__different_resource_id(self):
+        g.user = security_manager.get_guest_user_from_token(
+            {
+                "user": {},
+                "resources": [{"type": "dashboard", "id": self.resource_id - 1}],
+            }
+        )
+        has_guest_access = security_manager.has_guest_access(
+            GuestTokenResourceType.DASHBOARD, self.resource_id
+        )
+        self.assertFalse(has_guest_access)
+
+    def test_has_guest_access__unauthorized_guest_user__different_resource_type(self):
+        g.user = security_manager.get_guest_user_from_token(
+            {"user": {}, "resources": [{"type": "dirt", "id": self.resource_id}]}
+        )
+        has_guest_access = security_manager.has_guest_access(
+            GuestTokenResourceType.DASHBOARD, self.resource_id
+        )
+        self.assertFalse(has_guest_access)
+
+    def test_get_guest_user_roles_explicit(self):
+        guest = self.authorized_guest()
+        roles = security_manager.get_user_roles(guest)
+        self.assertEqual(guest.roles, roles)
+
+    def test_get_guest_user_roles_implicit(self):
+        guest = self.authorized_guest()
+        g.user = guest
+
+        roles = security_manager.get_user_roles()
+        self.assertEqual(guest.roles, roles)
+
+
+@mock.patch.dict(
+    "superset.extensions.feature_flag_manager._feature_flags", EMBEDDED_SUPERSET=True,
+)
+@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+class TestGuestUserDashboardAccess(SupersetTestCase):
+    def setUp(self) -> None:
+        self.dash = db.session.query(Dashboard).filter_by(slug="births").first()
+        self.authorized_guest = security_manager.get_guest_user_from_token(
+            {"user": {}, "resources": [{"type": "dashboard", "id": self.dash.id}]}
+        )
+        self.unauthorized_guest = security_manager.get_guest_user_from_token(
+            {"user": {}, "resources": [{"type": "dashboard", "id": self.dash.id + 1}]}
+        )
+
+    def test_chart_raise_for_access_as_guest(self):
+        chart = self.dash.slices[0]
+        g.user = self.authorized_guest
+
+        security_manager.raise_for_access(viz=chart)
+
+    def test_chart_raise_for_access_as_unauthorized_guest(self):
+        chart = self.dash.slices[0]
+        g.user = self.unauthorized_guest
+
+        with self.assertRaises(SupersetSecurityException):
+            security_manager.raise_for_access(viz=chart)
+
+    def test_dataset_raise_for_access_as_guest(self):
+        dataset = self.dash.slices[0].datasource
+        g.user = self.authorized_guest
+
+        security_manager.raise_for_access(datasource=dataset)
+
+    def test_dataset_raise_for_access_as_unauthorized_guest(self):
+        dataset = self.dash.slices[0].datasource
+        g.user = self.unauthorized_guest
+
+        with self.assertRaises(SupersetSecurityException):
+            security_manager.raise_for_access(datasource=dataset)
+
+    def test_guest_token_does_not_grant_access_to_underlying_table(self):
+        sqla_table = self.dash.slices[0].table
+        table = Table(table=sqla_table.table_name)
+
+        g.user = self.authorized_guest
+
+        with self.assertRaises(Exception):
+            security_manager.raise_for_access(table=table, database=sqla_table.database)
+
+    def test_raise_for_dashboard_access_as_guest(self):
+        g.user = self.authorized_guest
+
+        security_manager.raise_for_dashboard_access(self.dash)
+
+    def test_raise_for_dashboard_access_as_unauthorized_guest(self):
+        g.user = self.unauthorized_guest
+
+        with self.assertRaises(DashboardAccessDeniedError):
+            security_manager.raise_for_dashboard_access(self.dash)
diff --git a/tests/integration_tests/security/row_level_security_tests.py b/tests/integration_tests/security/row_level_security_tests.py
new file mode 100644
index 0000000..06610bb
--- /dev/null
+++ b/tests/integration_tests/security/row_level_security_tests.py
@@ -0,0 +1,316 @@
+# 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.
+# isort:skip_file
+import re
+from typing import Any, Dict, List, Optional
+from unittest import mock
+
+import pytest
+from flask import g
+
+from superset import db, security_manager
+from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable
+from superset.security.guest_token import (
+    GuestTokenRlsRule,
+    GuestTokenResourceType,
+    GuestUser,
+)
+from ..base_tests import SupersetTestCase
+from tests.integration_tests.fixtures.birth_names_dashboard import (
+    load_birth_names_dashboard_with_slices,
+    load_birth_names_data,
+)
+from tests.integration_tests.fixtures.energy_dashboard import (
+    load_energy_table_with_slice,
+    load_energy_table_data,
+)
+from tests.integration_tests.fixtures.unicode_dashboard import (
+    load_unicode_dashboard_with_slice,
+    load_unicode_data,
+)
+
+
+class TestRowLevelSecurity(SupersetTestCase):
+    """
+    Testing Row Level Security
+    """
+
+    rls_entry = None
+    query_obj: Dict[str, Any] = dict(
+        groupby=[],
+        metrics=None,
+        filter=[],
+        is_timeseries=False,
+        columns=["value"],
+        granularity=None,
+        from_dttm=None,
+        to_dttm=None,
+        extras={},
+    )
+    NAME_AB_ROLE = "NameAB"
+    NAME_Q_ROLE = "NameQ"
+    NAMES_A_REGEX = re.compile(r"name like 'A%'")
+    NAMES_B_REGEX = re.compile(r"name like 'B%'")
+    NAMES_Q_REGEX = re.compile(r"name like 'Q%'")
+    BASE_FILTER_REGEX = re.compile(r"gender = 'boy'")
+
+    def setUp(self):
+        session = db.session
+
+        # Create roles
+        self.role_ab = security_manager.add_role(self.NAME_AB_ROLE)
+        self.role_q = security_manager.add_role(self.NAME_Q_ROLE)
+        gamma_user = security_manager.find_user(username="gamma")
+        gamma_user.roles.append(self.role_ab)
+        gamma_user.roles.append(self.role_q)
+        self.create_user_with_roles("NoRlsRoleUser", ["Gamma"])
+        session.commit()
+
+        # Create regular RowLevelSecurityFilter (energy_usage, unicode_test)
+        self.rls_entry1 = RowLevelSecurityFilter()
+        self.rls_entry1.tables.extend(
+            session.query(SqlaTable)
+            .filter(SqlaTable.table_name.in_(["energy_usage", "unicode_test"]))
+            .all()
+        )
+        self.rls_entry1.filter_type = "Regular"
+        self.rls_entry1.clause = "value > {{ cache_key_wrapper(1) }}"
+        self.rls_entry1.group_key = None
+        self.rls_entry1.roles.append(security_manager.find_role("Gamma"))
+        self.rls_entry1.roles.append(security_manager.find_role("Alpha"))
+        db.session.add(self.rls_entry1)
+
+        # Create regular RowLevelSecurityFilter (birth_names name starts with A or B)
+        self.rls_entry2 = RowLevelSecurityFilter()
+        self.rls_entry2.tables.extend(
+            session.query(SqlaTable)
+            .filter(SqlaTable.table_name.in_(["birth_names"]))
+            .all()
+        )
+        self.rls_entry2.filter_type = "Regular"
+        self.rls_entry2.clause = "name like 'A%' or name like 'B%'"
+        self.rls_entry2.group_key = "name"
+        self.rls_entry2.roles.append(security_manager.find_role("NameAB"))
+        db.session.add(self.rls_entry2)
+
+        # Create Regular RowLevelSecurityFilter (birth_names name starts with Q)
+        self.rls_entry3 = RowLevelSecurityFilter()
+        self.rls_entry3.tables.extend(
+            session.query(SqlaTable)
+            .filter(SqlaTable.table_name.in_(["birth_names"]))
+            .all()
+        )
+        self.rls_entry3.filter_type = "Regular"
+        self.rls_entry3.clause = "name like 'Q%'"
+        self.rls_entry3.group_key = "name"
+        self.rls_entry3.roles.append(security_manager.find_role("NameQ"))
+        db.session.add(self.rls_entry3)
+
+        # Create Base RowLevelSecurityFilter (birth_names boys)
+        self.rls_entry4 = RowLevelSecurityFilter()
+        self.rls_entry4.tables.extend(
+            session.query(SqlaTable)
+            .filter(SqlaTable.table_name.in_(["birth_names"]))
+            .all()
+        )
+        self.rls_entry4.filter_type = "Base"
+        self.rls_entry4.clause = "gender = 'boy'"
+        self.rls_entry4.group_key = "gender"
+        self.rls_entry4.roles.append(security_manager.find_role("Admin"))
+        db.session.add(self.rls_entry4)
+
+        db.session.commit()
+
+    def tearDown(self):
+        session = db.session
+        session.delete(self.rls_entry1)
+        session.delete(self.rls_entry2)
+        session.delete(self.rls_entry3)
+        session.delete(self.rls_entry4)
+        session.delete(security_manager.find_role("NameAB"))
+        session.delete(security_manager.find_role("NameQ"))
+        session.delete(self.get_user("NoRlsRoleUser"))
+        session.commit()
+
+    @pytest.mark.usefixtures("load_energy_table_with_slice")
+    def test_rls_filter_alters_energy_query(self):
+        g.user = self.get_user(username="alpha")
+        tbl = self.get_table(name="energy_usage")
+        sql = tbl.get_query_str(self.query_obj)
+        assert tbl.get_extra_cache_keys(self.query_obj) == [1]
+        assert "value > 1" in sql
+
+    @pytest.mark.usefixtures("load_energy_table_with_slice")
+    def test_rls_filter_doesnt_alter_energy_query(self):
+        g.user = self.get_user(
+            username="admin"
+        )  # self.login() doesn't actually set the user
+        tbl = self.get_table(name="energy_usage")
+        sql = tbl.get_query_str(self.query_obj)
+        assert tbl.get_extra_cache_keys(self.query_obj) == []
+        assert "value > 1" not in sql
+
+    @pytest.mark.usefixtures("load_unicode_dashboard_with_slice")
+    def test_multiple_table_filter_alters_another_tables_query(self):
+        g.user = self.get_user(
+            username="alpha"
+        )  # self.login() doesn't actually set the user
+        tbl = self.get_table(name="unicode_test")
+        sql = tbl.get_query_str(self.query_obj)
+        assert tbl.get_extra_cache_keys(self.query_obj) == [1]
+        assert "value > 1" in sql
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_rls_filter_alters_gamma_birth_names_query(self):
+        g.user = self.get_user(username="gamma")
+        tbl = self.get_table(name="birth_names")
+        sql = tbl.get_query_str(self.query_obj)
+
+        # establish that the filters are grouped together correctly with
+        # ANDs, ORs and parens in the correct place
+        assert (
+            "WHERE ((name like 'A%'\n        or name like 'B%')\n       OR (name like 'Q%'))\n  AND (gender = 'boy');"
+            in sql
+        )
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_rls_filter_alters_no_role_user_birth_names_query(self):
+        g.user = self.get_user(username="NoRlsRoleUser")
+        tbl = self.get_table(name="birth_names")
+        sql = tbl.get_query_str(self.query_obj)
+
+        # gamma's filters should not be present query
+        assert not self.NAMES_A_REGEX.search(sql)
+        assert not self.NAMES_B_REGEX.search(sql)
+        assert not self.NAMES_Q_REGEX.search(sql)
+        # base query should be present
+        assert self.BASE_FILTER_REGEX.search(sql)
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_rls_filter_doesnt_alter_admin_birth_names_query(self):
+        g.user = self.get_user(username="admin")
+        tbl = self.get_table(name="birth_names")
+        sql = tbl.get_query_str(self.query_obj)
+
+        # no filters are applied for admin user
+        assert not self.NAMES_A_REGEX.search(sql)
+        assert not self.NAMES_B_REGEX.search(sql)
+        assert not self.NAMES_Q_REGEX.search(sql)
+        assert not self.BASE_FILTER_REGEX.search(sql)
+
+
+RLS_ALICE_REGEX = re.compile(r"name = 'Alice'")
+RLS_GENDER_REGEX = re.compile(r"AND \(gender = 'girl'\)")
+
+
+@mock.patch.dict(
+    "superset.extensions.feature_flag_manager._feature_flags", EMBEDDED_SUPERSET=True,
+)
+class GuestTokenRowLevelSecurityTests(SupersetTestCase):
+    query_obj: Dict[str, Any] = dict(
+        groupby=[],
+        metrics=None,
+        filter=[],
+        is_timeseries=False,
+        columns=["value"],
+        granularity=None,
+        from_dttm=None,
+        to_dttm=None,
+        extras={},
+    )
+
+    def default_rls_rule(self):
+        return {
+            "dataset": self.get_table(name="birth_names").id,
+            "clause": "name = 'Alice'",
+        }
+
+    def guest_user_with_rls(self, rules: Optional[List[Any]] = None) -> GuestUser:
+        if rules is None:
+            rules = [self.default_rls_rule()]
+        return security_manager.get_guest_user_from_token(
+            {
+                "user": {},
+                "resources": [{"type": GuestTokenResourceType.DASHBOARD.value}],
+                "rls_rules": rules,
+            }
+        )
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_rls_filter_alters_query(self):
+        g.user = self.guest_user_with_rls()
+        tbl = self.get_table(name="birth_names")
+        sql = tbl.get_query_str(self.query_obj)
+
+        self.assertRegexpMatches(sql, RLS_ALICE_REGEX)
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_rls_filter_does_not_alter_unrelated_query(self):
+        g.user = self.guest_user_with_rls(
+            rules=[
+                {
+                    "dataset": self.get_table(name="birth_names").id + 1,
+                    "clause": "name = 'Alice'",
+                }
+            ]
+        )
+        tbl = self.get_table(name="birth_names")
+        sql = tbl.get_query_str(self.query_obj)
+
+        self.assertNotRegexpMatches(sql, RLS_ALICE_REGEX)
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_multiple_rls_filters_are_unionized(self):
+        g.user = self.guest_user_with_rls(
+            rules=[
+                self.default_rls_rule(),
+                {
+                    "dataset": self.get_table(name="birth_names").id,
+                    "clause": "gender = 'girl'",
+                },
+            ]
+        )
+        tbl = self.get_table(name="birth_names")
+        sql = tbl.get_query_str(self.query_obj)
+
+        self.assertRegexpMatches(sql, RLS_ALICE_REGEX)
+        self.assertRegexpMatches(sql, RLS_GENDER_REGEX)
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    @pytest.mark.usefixtures("load_energy_table_with_slice")
+    def test_rls_filter_for_all_datasets(self):
+        births = self.get_table(name="birth_names")
+        energy = self.get_table(name="energy_usage")
+        guest = self.guest_user_with_rls(rules=[{"clause": "name = 'Alice'"}])
+        guest.resources.append({type: "dashboard", id: energy.id})
+        g.user = guest
+        births_sql = births.get_query_str(self.query_obj)
+        energy_sql = energy.get_query_str(self.query_obj)
+
+        self.assertRegexpMatches(births_sql, RLS_ALICE_REGEX)
+        self.assertRegexpMatches(energy_sql, RLS_ALICE_REGEX)
+
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_dataset_id_can_be_string(self):
+        dataset = self.get_table(name="birth_names")
+        str_id = str(dataset.id)
+        g.user = self.guest_user_with_rls(
+            rules=[{"dataset": str_id, "clause": "name = 'Alice'"}]
+        )
+        sql = dataset.get_query_str(self.query_obj)
+
+        self.assertRegexpMatches(sql, RLS_ALICE_REGEX)
diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py
index ab4450b..46ca679 100644
--- a/tests/integration_tests/security_tests.py
+++ b/tests/integration_tests/security_tests.py
@@ -16,23 +16,24 @@
 # under the License.
 # isort:skip_file
 import inspect
-import re
+import time
 import unittest
 from collections import namedtuple
 from unittest import mock
 from unittest.mock import Mock, patch
-from typing import Any, Dict
+from typing import Any
 
+import jwt
 import prison
 import pytest
 
-from flask import current_app, g
+from flask import current_app
 
 from superset.models.dashboard import Dashboard
 
 from superset import app, appbuilder, db, security_manager, viz, ConnectorRegistry
 from superset.connectors.druid.models import DruidCluster, DruidDatasource
-from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable
+from superset.connectors.sqla.models import SqlaTable
 from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.exceptions import SupersetSecurityException
 from superset.models.core import Database
@@ -46,22 +47,10 @@ from superset.utils.database import get_example_database
 from superset.views.access_requests import AccessRequestsModelView
 
 from .base_tests import SupersetTestCase
-from tests.integration_tests.fixtures.birth_names_dashboard import (
-    load_birth_names_dashboard_with_slices,
-    load_birth_names_data,
-)
-from tests.integration_tests.fixtures.energy_dashboard import (
-    load_energy_table_with_slice,
-    load_energy_table_data,
-)
 from tests.integration_tests.fixtures.public_role import (
     public_role_like_gamma,
     public_role_like_test_role,
 )
-from tests.integration_tests.fixtures.unicode_dashboard import (
-    load_unicode_dashboard_with_slice,
-    load_unicode_data,
-)
 from tests.integration_tests.fixtures.world_bank_dashboard import (
     load_world_bank_dashboard_with_slices,
     load_world_bank_data,
@@ -917,6 +906,7 @@ class TestRolePermission(SupersetTestCase):
             ["LocaleView", "index"],
             ["AuthDBView", "login"],
             ["AuthDBView", "logout"],
+            ["Dashboard", "embedded"],
             ["R", "index"],
             ["Superset", "log"],
             ["Superset", "theme"],
@@ -975,9 +965,7 @@ class TestSecurityManager(SupersetTestCase):
 
         mock_raise_for_access.side_effect = SupersetSecurityException(
             SupersetError(
-                "dummy",
-                SupersetErrorType.TABLE_SECURITY_ACCESS_ERROR,
-                ErrorLevel.ERROR,
+                "dummy", SupersetErrorType.TABLE_SECURITY_ACCESS_ERROR, ErrorLevel.ERROR
             )
         )
 
@@ -1054,174 +1042,18 @@ class TestSecurityManager(SupersetTestCase):
         with self.assertRaises(SupersetSecurityException):
             security_manager.raise_for_access(viz=test_viz)
 
+    @patch("superset.security.manager.g")
+    def test_get_user_roles(self, mock_g):
+        admin = security_manager.find_user("admin")
+        mock_g.user = admin
+        roles = security_manager.get_user_roles()
+        self.assertEqual(admin.roles, roles)
 
-class TestRowLevelSecurity(SupersetTestCase):
-    """
-    Testing Row Level Security
-    """
-
-    rls_entry = None
-    query_obj: Dict[str, Any] = dict(
-        groupby=[],
-        metrics=None,
-        filter=[],
-        is_timeseries=False,
-        columns=["value"],
-        granularity=None,
-        from_dttm=None,
-        to_dttm=None,
-        extras={},
-    )
-    NAME_AB_ROLE = "NameAB"
-    NAME_Q_ROLE = "NameQ"
-    NAMES_A_REGEX = re.compile(r"name like 'A%'")
-    NAMES_B_REGEX = re.compile(r"name like 'B%'")
-    NAMES_Q_REGEX = re.compile(r"name like 'Q%'")
-    BASE_FILTER_REGEX = re.compile(r"gender = 'boy'")
-
-    def setUp(self):
-        session = db.session
-
-        # Create roles
-        security_manager.add_role(self.NAME_AB_ROLE)
-        security_manager.add_role(self.NAME_Q_ROLE)
-        gamma_user = security_manager.find_user(username="gamma")
-        gamma_user.roles.append(security_manager.find_role(self.NAME_AB_ROLE))
-        gamma_user.roles.append(security_manager.find_role(self.NAME_Q_ROLE))
-        self.create_user_with_roles("NoRlsRoleUser", ["Gamma"])
-        session.commit()
-
-        # Create regular RowLevelSecurityFilter (energy_usage, unicode_test)
-        self.rls_entry1 = RowLevelSecurityFilter()
-        self.rls_entry1.tables.extend(
-            session.query(SqlaTable)
-            .filter(SqlaTable.table_name.in_(["energy_usage", "unicode_test"]))
-            .all()
-        )
-        self.rls_entry1.filter_type = "Regular"
-        self.rls_entry1.clause = "value > {{ cache_key_wrapper(1) }}"
-        self.rls_entry1.group_key = None
-        self.rls_entry1.roles.append(security_manager.find_role("Gamma"))
-        self.rls_entry1.roles.append(security_manager.find_role("Alpha"))
-        db.session.add(self.rls_entry1)
-
-        # Create regular RowLevelSecurityFilter (birth_names name starts with A or B)
-        self.rls_entry2 = RowLevelSecurityFilter()
-        self.rls_entry2.tables.extend(
-            session.query(SqlaTable)
-            .filter(SqlaTable.table_name.in_(["birth_names"]))
-            .all()
-        )
-        self.rls_entry2.filter_type = "Regular"
-        self.rls_entry2.clause = "name like 'A%' or name like 'B%'"
-        self.rls_entry2.group_key = "name"
-        self.rls_entry2.roles.append(security_manager.find_role("NameAB"))
-        db.session.add(self.rls_entry2)
-
-        # Create Regular RowLevelSecurityFilter (birth_names name starts with Q)
-        self.rls_entry3 = RowLevelSecurityFilter()
-        self.rls_entry3.tables.extend(
-            session.query(SqlaTable)
-            .filter(SqlaTable.table_name.in_(["birth_names"]))
-            .all()
-        )
-        self.rls_entry3.filter_type = "Regular"
-        self.rls_entry3.clause = "name like 'Q%'"
-        self.rls_entry3.group_key = "name"
-        self.rls_entry3.roles.append(security_manager.find_role("NameQ"))
-        db.session.add(self.rls_entry3)
-
-        # Create Base RowLevelSecurityFilter (birth_names boys)
-        self.rls_entry4 = RowLevelSecurityFilter()
-        self.rls_entry4.tables.extend(
-            session.query(SqlaTable)
-            .filter(SqlaTable.table_name.in_(["birth_names"]))
-            .all()
-        )
-        self.rls_entry4.filter_type = "Base"
-        self.rls_entry4.clause = "gender = 'boy'"
-        self.rls_entry4.group_key = "gender"
-        self.rls_entry4.roles.append(security_manager.find_role("Admin"))
-        db.session.add(self.rls_entry4)
-
-        db.session.commit()
-
-    def tearDown(self):
-        session = db.session
-        session.delete(self.rls_entry1)
-        session.delete(self.rls_entry2)
-        session.delete(self.rls_entry3)
-        session.delete(self.rls_entry4)
-        session.delete(security_manager.find_role("NameAB"))
-        session.delete(security_manager.find_role("NameQ"))
-        session.delete(self.get_user("NoRlsRoleUser"))
-        session.commit()
-
-    @pytest.mark.usefixtures("load_energy_table_with_slice")
-    def test_rls_filter_alters_energy_query(self):
-        g.user = self.get_user(username="alpha")
-        tbl = self.get_table(name="energy_usage")
-        sql = tbl.get_query_str(self.query_obj)
-        assert tbl.get_extra_cache_keys(self.query_obj) == [1]
-        assert "value > 1" in sql
-
-    @pytest.mark.usefixtures("load_energy_table_with_slice")
-    def test_rls_filter_doesnt_alter_energy_query(self):
-        g.user = self.get_user(
-            username="admin"
-        )  # self.login() doesn't actually set the user
-        tbl = self.get_table(name="energy_usage")
-        sql = tbl.get_query_str(self.query_obj)
-        assert tbl.get_extra_cache_keys(self.query_obj) == []
-        assert "value > 1" not in sql
-
-    @pytest.mark.usefixtures("load_unicode_dashboard_with_slice")
-    def test_multiple_table_filter_alters_another_tables_query(self):
-        g.user = self.get_user(
-            username="alpha"
-        )  # self.login() doesn't actually set the user
-        tbl = self.get_table(name="unicode_test")
-        sql = tbl.get_query_str(self.query_obj)
-        assert tbl.get_extra_cache_keys(self.query_obj) == [1]
-        assert "value > 1" in sql
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    def test_rls_filter_alters_gamma_birth_names_query(self):
-        g.user = self.get_user(username="gamma")
-        tbl = self.get_table(name="birth_names")
-        sql = tbl.get_query_str(self.query_obj)
-
-        # establish that the filters are grouped together correctly with
-        # ANDs, ORs and parens in the correct place
-        assert (
-            "WHERE ((name like 'A%'\n        or name like 'B%')\n       OR (name like 'Q%'))\n  AND (gender = 'boy');"
-            in sql
-        )
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    def test_rls_filter_alters_no_role_user_birth_names_query(self):
-        g.user = self.get_user(username="NoRlsRoleUser")
-        tbl = self.get_table(name="birth_names")
-        sql = tbl.get_query_str(self.query_obj)
-
-        # gamma's filters should not be present query
-        assert not self.NAMES_A_REGEX.search(sql)
-        assert not self.NAMES_B_REGEX.search(sql)
-        assert not self.NAMES_Q_REGEX.search(sql)
-        # base query should be present
-        assert self.BASE_FILTER_REGEX.search(sql)
-
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    def test_rls_filter_doesnt_alter_admin_birth_names_query(self):
-        g.user = self.get_user(username="admin")
-        tbl = self.get_table(name="birth_names")
-        sql = tbl.get_query_str(self.query_obj)
-
-        # no filters are applied for admin user
-        assert not self.NAMES_A_REGEX.search(sql)
-        assert not self.NAMES_B_REGEX.search(sql)
-        assert not self.NAMES_Q_REGEX.search(sql)
-        assert not self.BASE_FILTER_REGEX.search(sql)
+    @patch("superset.security.manager.g")
+    def test_get_anonymous_roles(self, mock_g):
+        mock_g.user = security_manager.get_anonymous_user()
+        roles = security_manager.get_user_roles()
+        self.assertEqual([security_manager.get_public_role()], roles)
 
 
 class TestAccessRequestEndpoints(SupersetTestCase):
@@ -1323,3 +1155,89 @@ class TestDatasources(SupersetTestCase):
             Datasource("database1", "schema1", "table1"),
             Datasource("database1", "schema1", "table2"),
         ]
+
+
+class FakeRequest:
+    headers: Any = {}
+
+
+class TestGuestTokens(SupersetTestCase):
+    def create_guest_token(self):
+        user = {"username": "test_guest"}
+        resources = [{"some": "resource"}]
+        rls = [{"dataset": 1, "clause": "access = 1"}]
+        return security_manager.create_guest_access_token(user, resources, rls)
+
+    @patch("superset.security.SupersetSecurityManager._get_current_epoch_time")
+    def test_create_guest_access_token(self, get_time_mock):
+        now = time.time()
+        get_time_mock.return_value = now  # so we know what it should =
+
+        user = {"username": "test_guest"}
+        resources = [{"some": "resource"}]
+        rls = [{"dataset": 1, "clause": "access = 1"}]
+        token = security_manager.create_guest_access_token(user, resources, rls)
+
+        # unfortunately we cannot mock time in the jwt lib
+        decoded_token = jwt.decode(
+            token,
+            self.app.config["GUEST_TOKEN_JWT_SECRET"],
+            algorithms=[self.app.config["GUEST_TOKEN_JWT_ALGO"]],
+        )
+
+        self.assertEqual(user, decoded_token["user"])
+        self.assertEqual(resources, decoded_token["resources"])
+        self.assertEqual(now, decoded_token["iat"])
+        self.assertEqual(
+            now + (self.app.config["GUEST_TOKEN_JWT_EXP_SECONDS"] * 1000),
+            decoded_token["exp"],
+        )
+
+    def test_get_guest_user(self):
+        token = self.create_guest_token()
+        fake_request = FakeRequest()
+        fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token
+
+        guest_user = security_manager.get_guest_user_from_request(fake_request)
+
+        self.assertIsNotNone(guest_user)
+        self.assertEqual("test_guest", guest_user.username)
+
+    @patch("superset.security.SupersetSecurityManager._get_current_epoch_time")
+    def test_get_guest_user_expired_token(self, get_time_mock):
+        # make a just-expired token
+        get_time_mock.return_value = (
+            time.time() - (self.app.config["GUEST_TOKEN_JWT_EXP_SECONDS"] * 1000) - 1
+        )
+        token = self.create_guest_token()
+        fake_request = FakeRequest()
+        fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token
+
+        guest_user = security_manager.get_guest_user_from_request(fake_request)
+
+        self.assertIsNone(guest_user)
+
+    def test_get_guest_user_no_user(self):
+        user = None
+        resources = [{"type": "dashboard", "id": 1}]
+        rls = {}
+        token = security_manager.create_guest_access_token(user, resources, rls)
+        fake_request = FakeRequest()
+        fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token
+        guest_user = security_manager.get_guest_user_from_request(fake_request)
+
+        self.assertIsNone(guest_user)
+        self.assertRaisesRegex(ValueError, "Guest token does not contain a user claim")
+
+    def test_get_guest_user_no_resource(self):
+        user = {"username": "test_guest"}
+        resources = []
+        rls = {}
+        token = security_manager.create_guest_access_token(user, resources, rls)
+        fake_request = FakeRequest()
+        fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token
+        security_manager.get_guest_user_from_request(fake_request)
+
+        self.assertRaisesRegex(
+            ValueError, "Guest token does not contain a resources claim"
+        )