superset icon indicating copy to clipboard operation
superset copied to clipboard

403 Forbidden error when trying to embed a dashboard & view it using a guest user token

Open adimyth opened this issue 2 years ago • 1 comments

I am trying to embed a superset dashboard into a simple front-end application. However, when the embedded dashboard always throws 403: Forbidden. I am pretty sure I have set the correct permissions and everything. I even tried accessing it using admin user, still it fails.

How to reproduce the bug

The source for the application can be found here - https://github.com/adimyth/superset_embedded

superset application

Added the following configuration in superset_config.py

FEATURE_FLAGS = {"ALERT_REPORTS": True, "EMBEDDED_SUPERSET": True}

SESSION_COOKIE_SAMESITE = None
ENABLE_PROXY_FIX = True
PUBLIC_ROLE_LIKE_GAMMA = True
CORS_OPTIONS = {
    'supports_credentials': True,
    'allow_headers': ['*'],
    'resources': ['*'],
    'origins': ['http://localhost:8088', 'http://localhost:8000']
}

This gave me the following dashboard it to embed into my application Screenshot 2022-11-29 at 2 28 54 PM

frontend application

<head>
    <script src="https://unpkg.com/@preset-sdk/embedded"></script>
    <style>
        iframe {
            height: 100%;
            width: 100%;
            border: none;
        }
    </style>
</head>
<body>
    <p id="dashboard-container"> </p>
    <script>
        // 1. Request guest_token from our backend, which runs at localhost:8000 by default 
            async function fetchGuestTokenFromBackend() {
                let response = await fetch('http://127.0.0.1:8000/guest-token');
                let data = await response.json()
                return data
        }

        // 2. Uses Preset Embedded SDK to embed the dashboard as iFrame
        const myDashboard = presetSdk.embedDashboard({
          id: "{{ DASHBOARD_ID }}",
          supersetDomain: "{{ SUPERSET_DOMAIN }}",
          mountPoint: document.getElementById("dashboard-container"),
          fetchGuestToken: () => fetchGuestTokenFromBackend(),
          dashboardUiConfig: { hideTitle: true, hideChartControls: true}
        });
    </script>
</body>

The frontend simply calls the /guest-token from backend and passes it to the presetSdk.embedDashboard

backend application

The /guest-token endpoint simply calls 2 methods from the helper (superset.py) file

@app.get("/guest-token")
async def analytics_view(request: Request):
    access_token = superset.authenticate()
    guest_token = superset.get_guest_token_for_dashboard(
        dashboard_id=DASHBOARD_ID, access_token=access_token
    )
    return guest_token

The helper has 2 methods - authenticate() which authenticates the superset-admin user & get_guest_token_for_dashboard which is used to generate access token for the guest user

import json
import os

import requests
from dotenv import load_dotenv

load_dotenv()


URL_AUTH = os.getenv("URL_AUTH")
URL_GUEST_TOKEN = os.getenv("URL_GUEST_TOKEN")
USERNAME = os.getenv("USERNAME")
FIRST_NAMER = os.getenv("FIRST_NAMER")
LAST_NAME = os.getenv("LAST_NAME")


def authenticate(
    username="admin",
    password="admin",
):
    response = requests.post(
        "http://localhost:8088/api/v1/security/login",
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Access-Control-Allow-Origin": "http://localhost:8000",
        },
        data=json.dumps(
            {
                "username": username,
                "password": password,
                "provider": "db"
            }
        ),
    )
    return response.json()["access_token"]


def get_guest_token_for_dashboard(
    dashboard_id,
    access_token,
    username=USERNAME,
    first_name=FIRST_NAMER,
    last_name=LAST_NAME,
):
    response = requests.post(
        URL_GUEST_TOKEN,
        data=json.dumps(
            {
                "user": {
                    "username": username,
                    "first_name": first_name,
                    "last_name": last_name,
                },
                "resources": [
                    {
                        "type": "dashboard",
                        "id": dashboard_id,
                    }
                ],
                "rls": [],
            }
        ),
        headers={
            "Authorization": "Bearer " + access_token,
            "Accept": "application/json",
            "Content-Type": "application/json",
        },
    )
    return response.json()["token"]

Finally we pass environment variables. Note that the DASHBOARD_ID matches from the screenshot above. Also note that I am passing superset-admin creds to the embedded dashboard

URL_AUTH=http://localhost:8088/api/v1/security/login
URL_GUEST_TOKEN=http://localhost:8088/api/v1/security/guest_token/
USERNAME=admin
FIRST_NAMER=Supserset
LAST_NAME=Admin
DASHBOARD_ID=b0a944b2-4ab5-47b7-a31d-c3eca4c36397
SUPERSET_DOMAIN=http://localhost:8088/

Error

When loading the frontend application which has the embedded dashboard, it returns -

{"errors": [{"message": "403 Forbidden: You don't have the permission to access the requested resource. It is either read-protected or not readable by the server.", "error_type": "GENERIC_BACKEND_ERROR", "level": "error", "extra": {"issue_codes": [{"code": 1011, "message": "Issue 1011 - Superset encountered an unexpected error."}]}}]}

Expected results

I was expecting to see the actual dashboard in the frontend application

Actual results

Screenshot 2022-11-29 at 2 37 17 PM

What's surprising is that after decoding the guest token, we can clearly see that the user is superset admin & has access to the dashboard 🤯 Screenshot 2022-11-29 at 2 39 15 PM

Environment

(please complete the following information):

  • browser type and version: Chrome
  • superset version: Built from the latest code as of 29th Nov
  • python version: 3.9
  • node.js version: NA
  • any feature flags active: {"ALERT_REPORTS": True, "EMBEDDED_SUPERSET": True}

Checklist

Make sure to follow these steps before submitting your issue - thank you!

  • [x] I have checked the superset logs for python stacktraces and included it here as text if there are any.
  • [x] I have reproduced the issue with at least the latest released version of superset.
  • [x] I have checked the issue tracker for the same issue and I haven't found one similar.

adimyth avatar Nov 29 '22 09:11 adimyth

Pinging @lilykuang here, but also sharing this on the Superset Slack workspace in the #embedding-superset channel in case anyone there has insight. Let me know if you would like help connecting there as well to join the thread.

rusackas avatar Nov 30 '22 17:11 rusackas

The first thing to try would be to remove ALL the allowed domains and leave the field empty.

Background: The allowed domains field is incorrectly labeled in the UI. What this field does is to check the "Referer" header and compare it against the values in this field, which is IMO a bit awkward.

Relevant discussion in Slack. https://apache-superset.slack.com/archives/C01EP56QGTS/p1663143592850569?thread_ts=1663071066.037159&cid=C01EP56QGTS

If that doesn't help, the next step is to investigate the Guest permissions configuration.

cwegener avatar Nov 30 '22 20:11 cwegener

Hey! Thanks for the response! Yeah, I would like to join the apache superset slack channel to discuss this & other issues further

adimyth avatar Dec 01 '22 08:12 adimyth

The first thing to try would be to remove ALL the allowed domains and leave the field empty.

Background: The allowed domains field is incorrectly labeled in the UI. What this field does is to check the "Referer" header and compare it against the values in this field, which is IMO a bit awkward.

Relevant discussion in Slack. https://apache-superset.slack.com/archives/C01EP56QGTS/p1663143592850569?thread_ts=1663071066.037159&cid=C01EP56QGTS

If that doesn't help, the next step is to investigate the Guest permissions configuration.

Adding the following in superset_config.py seems to be working for me -

# Dashboard embedding
GUEST_ROLE_NAME = "Gamma"
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

I shall test this out & revert!

adimyth avatar Dec 01 '22 08:12 adimyth

Yup! GUEST_ROLE_NAME = "Gamma" will give the Guest Token all the required (and much more) Superset permissions in order to access the required Superset API functions.

The exact permissions to use for setting up your own custom role in Superset have been recently documented in a Github discussion here: https://github.com/apache/superset/discussions/18814#discussioncomment-4056030

cwegener avatar Dec 01 '22 11:12 cwegener

For posterity, just linking a Slack conversation related to this issue here

rusackas avatar Dec 06 '22 18:12 rusackas

Hi,

My Dashboard is getting embedding successfully in firefox, but on chrome it is giving error 400 for CSRF.

I have done the below config changes

SESSION_COOKIE_HTTPONLY = True # Prevent cookie from being read by frontend JS? SESSION_COOKIE_SECURE = True # Prevent cookie from being transmitted over non-tls? SESSION_COOKIE_SAMESITE = None # One of [None, 'None', 'Lax', 'Strict'] GUEST_ROLE_NAME = "Gamma" PUBLIC_ROLE_LIKE_GAMMA= True

ENABLE_PROXY_FIX = True ENABLE_CORS = True CORS_OPTIONS: Dict[Any, Any] = { 'supports_credentials': True, 'allow_headers': [''], 'resources':[''], 'origins': ['*'] }

Can anyone help with what am i missing?

tejaskatariya avatar Jan 10 '23 09:01 tejaskatariya

I have added a blog to implement RLS with a custom role and limited permissions - https://medium.com/@vishalsadriya1224/embedding-apache-superset-dashboards-in-ruby-on-rails-and-react-a-role-level-security-guide-697da01676af

vishaltps avatar Oct 16 '23 12:10 vishaltps

After set GUEST_ROLE_NAME = 'Gamma' you must to set permission in gama role to grant guest token

image

httpie-salesrun avatar May 06 '24 23:05 httpie-salesrun

Hey! Thanks for the response! Yeah, I would like to join the apache superset slack channel to discuss this & other issues further

I have encountered the same problem. How can I solve it? Configuration is as follows # cors WTF_CSRF_ENABLED = False TALISMAN_ENABLED = False ENABLE_PROXY_FIX = True PUBLIC_ROLE_LIKE_GAMMA = True GUEST_ROLE_NAME = 'Gamma' TALISMAN_ENABLED = False ENABLE_CORS = True CORS_OPTIONS = { "supports_credentials": True, "allow_headers": "", "expose_headers": "", "resources": "", "origins": "" }

dongdong1103 avatar Jul 11 '24 09:07 dongdong1103