# Superset Authentication Analysis > Анализ механизмов аутентификации Apache Superset для интеграции с ss-tools. > Цель: замена UI-логина в Playwright ScreenshotService на API-based login (ADFS-совместимость). --- ## 1. Общая архитектура аутентификации Superset ``` ┌──────────────────────────────────────┐ │ UI Browser Login │ │ POST /login/ (username+password) │ ← AuthDBView │ POST /login/ (OAuth) │ ← AuthOAuthView │ GET /oauth-authorized/ │ ← 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 ### 2.1. Flask-Login Session Cookie (UI) | Свойство | Значение | |----------|----------| | Механизм | Flask-Login `login_user()` | | Cookie | `session` (signed, HttpOnly) | | Создаётся | `POST /login/`, `POST /login/`, `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.py` — `get_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.py` — `LoginPost` schema - `/home/busya/.local/lib/python3.13/site-packages/flask_appbuilder/security/manager.py` — `auth_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` — параметры ```json 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.py` — `SecurityRestApi.guest_token()` - `/home/busya/dev/superset/superset/security/manager.py` — `create_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) ```python 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`: ```python 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` ```python 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) ```python 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 (ss-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]` ```python @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 ```python 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//`) **Файл:** `/home/busya/dev/superset/superset/views/core.py` (строки 771-831) ```python class Superset(BaseSupersetView): @has_access # ← только session cookie! JWT не принимает! @expose("/dashboard//") 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//embedded`) **Файл:** `/home/busya/dev/superset/superset/views/dashboard/views.py` ```python class Dashboard(BaseSupersetView): @expose("//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/`) **Файл:** `/home/busya/dev/superset/superset/embedded/view.py` ```python class EmbeddedView(BaseSupersetView): @expose("/") 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 ```http POST /api/v1/security/guest_token/ Authorization: Bearer { "user": {"username": "admin", "first_name": "Admin", "last_name": ""}, "resources": [{"type": "dashboard", "id": ""}], "rls": [] } ``` Ответ: ```json { "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/` (анонимная сессия) 3. SPA на клиенте использует `X-GuestToken` для API-вызовов **Но:** для отображения дашборда **нельзя** просто открыть `/superset/dashboard//` с `X-GuestToken` — этот эндпоинт не принимает guest token. ### 7.3. Guest token security ```python # 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 ) → guest_token 3. page.goto("/superset/dashboard//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//?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//`) | | `superset/views/dashboard/views.py` | Embedded dashboard view | | `superset/embedded/view.py` | `EmbeddedView` (`/embedded/`) | | `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 |