Files
ss-tools/docs/superset-auth-analysis.md
busya ec6421de35 rename ss-tools to superset-tools across the entire project
- Replace all occurrences of 'ss-tools' with 'superset-tools' in 104 files
- Rename git bundle file ss-tools.bundle → superset-tools.bundle
- Update .gitignore pattern accordingly
- Preserve variable names (hasSsTools etc.) and code identifiers
2026-06-16 11:15:19 +03:00

18 KiB
Raw Permalink Blame History

Superset Authentication Analysis

Анализ механизмов аутентификации Apache Superset для интеграции с superset-tools. Цель: замена UI-логина в Playwright ScreenshotService на API-based login (ADFS-совместимость).


1. Общая архитектура аутентификации Superset

┌──────────────────────────────────────┐
│         UI Browser Login             │
│  POST /login/ (username+password)    │  ← AuthDBView
│  POST /login/<provider> (OAuth)      │  ← AuthOAuthView
│  GET  /oauth-authorized/<provider>   │  ← OAuth callback
└──────────────┬───────────────────────┘
               │ login_user(user)       ← Flask-Login создаёт session cookie
               ▼
┌──────────────────────────────────────┐
│  Flask Session Cookie ("session")    │
│  Используется @has_access (MVC)      │
│  Используется @protect(allow_browser)│
└──────────────────────────────────────┘

┌──────────────────────────────────────┐
│     REST API Login                   │
│  POST /api/v1/security/login         │
│  {username, password, provider, refresh}│
└──────────────┬───────────────────────┘
               │ create_access_token()  ← flask-jwt-extended
               ▼
┌──────────────────────────────────────┐
│  JWT Bearer Token (access_token)     │
│  Используется @protect()             │
│  Refresh: POST /api/v1/security/refresh│
└──────────────────────────────────────┘

┌──────────────────────────────────────┐
│  Embedded / Guest Auth               │
│  POST /api/v1/security/guest_token/  │
│  {user, resources, rls}              │
└──────────────┬───────────────────────┘
               │ create_guest_access_token()
               ▼
┌──────────────────────────────────────┐
│  Guest JWT (X-GuestToken header)     │
│  Парсится request_loader()           │
│  Живёт 5 мин (дефолт)                │
└──────────────────────────────────────┘

2. Три независимых auth-системы в Superset

Свойство Значение
Механизм Flask-Login login_user()
Cookie session (signed, HttpOnly)
Создаётся POST /login/, POST /login/<provider>, GET /oauth-authorized/
Используется @has_access декоратор (MVC views)
REST API @protect(allow_browser_login=True) — принимает session ИЛИ JWT

Файлы:

  • /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/views.py — AuthDBView, AuthOAuthView
  • /home/busya/dev/superset/superset/utils/machine_auth.pyget_auth_cookies() — программное создание session cookie

2.2. Flask-JWT-Extended (REST API)

Свойство Значение
Механизм flask-jwt-extended
Токен access_token (Bearer) + refresh_token
Secret SECRET_KEY (Flask)
expires 30 мин (default)
Создаётся POST /api/v1/security/login
Используется @protect() (REST API)

Файлы:

  • /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/api.py — класс SecurityApi, endpoint /api/v1/security/login
  • /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/schemas.pyLoginPost schema
  • /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/manager.pyauth_user_db(), auth_user_ldap()
  • /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/decorators.py@protect() decorator

2.2.1. /api/v1/security/login — параметры

POST /api/v1/security/login
{
  "username": "admin",          // required
  "password": "admin123",       // required
  "provider": "db",             // optional: "db" | "ldap"
  "refresh": true               // optional: вернуть refresh_token
}
 { "access_token": "...", "refresh_token": "..." }

Важно: provider может быть только "db" или "ldap". OAuth/OpenID/ADFS недоступны через REST API.

2.3. Guest Token (Embedded)

Свойство Значение
Механизм PyJWT (не flask-jwt-extended)
Secret GUEST_TOKEN_JWT_SECRET (отдельный)
expires 300 сек (default)
Header X-GuestToken
Создаётся POST /api/v1/security/guest_token/
Используется request_loader() в SupersetSecurityManager

Файлы:

  • /home/busya/dev/superset/superset/security/api.pySecurityRestApi.guest_token()
  • /home/busya/dev/superset/superset/security/manager.pycreate_guest_access_token(), request_loader()

3. REST API Login детально

3.1. Endpoint

Файл: /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/api.py (строки 32-114)

class SecurityApi(BaseApi):
    resource_name = "security"
    version = "v1"
    allow_browser_login = True  # ← принимает session cookie ИЛИ JWT

    @expose("/login", methods=["POST"])
    def login(self):
        login_payload = self.login_post_schema.load(request.json)
        if login_payload["provider"] == "db":
            user = self.appbuilder.sm.auth_user_db(
                login_payload["username"],
                login_payload["password"],
            )
        elif login_payload["provider"] == "ldap":
            user = self.appbuilder.sm.auth_user_ldap(
                login_payload["username"],
                login_payload["password"],
            )
        if not user:
            return self.response_401(message="...")

        resp = {
            "access_token": create_access_token(identity=str(user.id), fresh=True)
        }
        if login_payload.get("refresh"):
            resp["refresh_token"] = create_refresh_token(identity=str(user.id))
        return self.response(200, **resp)

3.2. Что происходит после получения JWT

  • login_user() НЕ вызывается → Flask session cookie НЕ создаётся
  • Только create_access_token() — возвращает JWT для REST API
  • Для UI-навигации (dashboard rendering) нужна session cookie, а не JWT

4. ADFS / OAuth login flow

4.1. Конфигурация

В /home/busya/dev/superset/superset/config.py:

AUTH_TYPE = AUTH_OAUTH  # = 4  (FAB константа)
OAUTH_PROVIDERS = [
    {
        "name": "adfs",
        "icon": "fa-windows",
        "token_key": "access_token",
        "remote_app": {
            "client_id": "...",
            "client_secret": "...",
            "server_metadata_url": "https://adfs.example.com/.well-known/openid-configuration",
            "api_base_url": "https://adfs.example.com/",
        },
    }
]

Файл констант: /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/const.py

AUTH_DB = 1
AUTH_LDAP = 2
AUTH_REMOTE_USER = 3
AUTH_OAUTH = 4

4.2. OAuth login flow (UI browser)

Файл: /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/views.py (строки 624-729)

1. GET  /login/adfs            → redirect to ADFS (state + redirect_uri)
2. User auth on ADFS            → ADFS выставляет cookie
3. GET  /oauth-authorized/adfs  → ADFS redirects back with code
4. POST adfs/token              → Superset получает access_token от ADFS
5. sm.auth_user_oauth(userinfo) → создаёт/обновляет пользователя в БД
6. login_user(user)             → Flask session cookie
7. Redirect to next_url

Ключевой момент: ADFS/SSO недоступен через REST API (/api/v1/security/login). Только через browser flow с redirect.

4.3. auth_user_oauth()

Файл: /home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/manager.py (строки 1423-1480)

def auth_user_oauth(self, userinfo):
    username = userinfo.get("username") or userinfo.get("email")
    user = self.find_user(username=username)
    if not user:
        user = self.add_user(...)
    login_user(user)  # ← только здесь создаётся session cookie
    return user

5. Machine Auth Provider (superset-tools: machine_auth.py)

Файл: /home/busya/dev/superset/superset/utils/machine_auth.py

Этот модуль создан специально для программного получения session cookie (используется для thumbnails, reports).

5.1. get_auth_cookies(user: User) -> dict[str, str]

@staticmethod
def get_auth_cookies(user):
    with current_app.test_request_context("/login"):
        login_user(user)            # Flask-Login
        response = Response()
        current_app.process_response(response)
        current_app.session_interface.save_session(current_app, session, response)

    cookies = {}
    for name, value in response.headers:
        if name.lower() == "set-cookie":
            cookie = parse_cookie(value)
            cookie_tuple = list(cookie.items())[0]
            cookies[cookie_tuple[0]] = cookie_tuple[1]
    return cookies

5.2. ProviderType enum

class ProviderType(enum.Enum):
    DB = "db"          # username/password
    LDAP = "ldap"      # LDAP
    OAUTH2 = "oauth2"  # OAuth token for database access (не для Superset login!)

6. Dashboard Rendering — проверка auth

6.1. UI Dashboard (/superset/dashboard/<id>/)

Файл: /home/busya/dev/superset/superset/views/core.py (строки 771-831)

class Superset(BaseSupersetView):
    @has_access  # ← только session cookie! JWT не принимает!
    @expose("/dashboard/<dashboard_id_or_slug>/")
    def dashboard(self, dashboard_id_or_slug):
        ...
        return self.render_template("superset/spa.html", entry="spa", ...)

Ключевой момент: декоратор @has_access (из FAB) проверяет g.user и current_user.is_authenticated (Flask-Login). Если user не аутентифицирован → редирект на /login.

6.2. Embedded Dashboard (/superset/dashboard/<id>/embedded)

Файл: /home/busya/dev/superset/superset/views/dashboard/views.py

class Dashboard(BaseSupersetView):
    @expose("/<dashboard_id_or_slug>/embedded")
    def embedded(self, dashboard_id_or_slug):
        if not is_feature_enabled("EMBEDDED_SUPERSET"):
            return Response(status=404)
        login_user(AnonymousUserMixin(), force=True)  # ← anonymous session
        bootstrap_data = {
            "embedded": {"dashboard_id": dashboard.id},
        }
        return self.render_template("superset/spa.html", entry="embedded", ...)

Требует EMBEDDED_SUPERSET feature flag. Использует анонимную сессию + guest token для API-запросов из SPA.

6.3. Embedded View (/embedded/<uuid>)

Файл: /home/busya/dev/superset/superset/embedded/view.py

class EmbeddedView(BaseSupersetView):
    @expose("/<uuid>")
    def embedded(self, uuid):
        if not is_feature_enabled("EMBEDDED_SUPERSET"):
            abort(404)
        login_user(AnonymousUserMixin(), force=True)
        bootstrap_data = {
            "embedded": {"dashboard_id": embedded.dashboard_id},
        }
        return self.render_template("superset/spa.html", entry="embedded", ...)

7. Guest Token — детально

7.1. Создание guest token

POST /api/v1/security/guest_token/
Authorization: Bearer <access_token>

{
  "user": {"username": "admin", "first_name": "Admin", "last_name": ""},
  "resources": [{"type": "dashboard", "id": "<dashboard_id>"}],
  "rls": []
}

Ответ:

{
  "token": "<jwt_guest_token>"
}

7.2. Использование guest token

Guest token передаётся в заголовке X-GuestToken (настраивается через GUEST_TOKEN_HEADER_NAME).

SPA (JavaScript) отправляет его во все API-запросы.

Для рендеринга дашборда через guest token:

  1. Получить guest token через REST API (требует обычный JWT)
  2. Открыть /embedded/<uuid> (анонимная сессия)
  3. SPA на клиенте использует X-GuestToken для API-вызовов

Но: для отображения дашборда нельзя просто открыть /superset/dashboard/<id>/ с X-GuestToken — этот эндпоинт не принимает guest token.

7.3. Guest token security

# superset/config.py
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

8. @protect() vs @has_access — разница

Характеристика @protect() @has_access()
Где используется REST API (BaseSupersetApi) MVC views (BaseSupersetView)
Проверяет JWT Bearer token (через verify_jwt_in_request()) Flask session cookie (через current_user.is_authenticated)
allow_browser_login Если True, принимает session cookie ИЛИ JWT Не применимо
Редирект при неудаче HTTP 401 Редирект на /login/
Источник FAB decorators.py FAB decorators.py

9. Сравнение подходов для Playwright ScreenshotService

Вариант A: Guest Token + Embedded

1. POST /api/v1/security/login (provider: "db") → access_token (JWT)
2. POST /api/v1/security/guest_token/ (с Bearer <JWT>) → guest_token
3. page.goto("/superset/dashboard/<id>/embedded") → anonymous session
4. context.set_extra_http_headers({"X-GuestToken": "..."})
5. SPA загружается, использует guest token для API → скриншот

Требования:

  • EMBEDDED_SUPERSET = True
  • GUEST_TOKEN_JWT_SECRET настроен
  • Dashboard может требовать embedded config

Вариант B: JWT + Direct Form POST

1. POST /api/v1/security/login → access_token (JWT)
2. POST /login/ через page.context.request.post() с username/password
3. Перехватить Set-Cookie: session=... из response
4. context.add_cookies() → установить session cookie
5. page.goto("/superset/dashboard/<id>/?standalone=true")

Требования:

  • AUTH_TYPE включает AUTH_DB (или db auth не отключён)
  • POST /login/ не редиректит на ADFS

Вариант C: JWT + API-only (без UI)

1. POST /api/v1/security/login → access_token
2. Запросить данные дашборда через REST API
3. Собрать screenshot из данных (без browser)

Недостатки:

  • Не отображает реальный UI
  • Потеря визуального контекста для LLM

10. Ключевые файлы и их расположение

Файл Назначение
flask_appbuilder/security/api.py FAB REST API login (/api/v1/security/login)
flask_appbuilder/security/views.py FAB UI login views (DB, LDAP, OAuth, OpenID)
flask_appbuilder/security/manager.py auth_user_db(), auth_user_ldap(), auth_user_oauth()
flask_appbuilder/security/decorators.py @protect() и @has_access()
flask_appbuilder/security/schemas.py LoginPost marshmallow schema
flask_appbuilder/const.py Auth type constants (AUTH_DB=1, AUTH_OAUTH=4)
superset/security/manager.py SupersetSecurityManager — guest tokens, RLS, ownership
superset/security/api.py SecurityRestApi — CSRF token, guest token endpoints
superset/security/guest_token.py Guest token типы (GuestToken, GuestUser)
superset/config.py AUTH_TYPE, GUEST_TOKEN_JWT_SECRET, feature flags
superset/views/core.py Dashboard rendering (/superset/dashboard/<id>/)
superset/views/dashboard/views.py Embedded dashboard view
superset/embedded/view.py EmbeddedView (/embedded/<uuid>)
superset/embedded/api.py EmbeddedDashboardRestApi
superset/views/base_api.py BaseSupersetApi, BaseSupersetModelRestApi
superset/utils/machine_auth.py MachineAuthProvider — программное создание session cookie
superset/utils/oauth2.py OAuth2 для database access (не для Superset login)
superset/initialization/__init__.py App init, FAB config, API registration
superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts Frontend SupersetClient — csrf, guest token, session cookie handling