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

445 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
### 2.1. Flask-Login Session Cookie (UI)
| Свойство | Значение |
|----------|----------|
| Механизм | 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.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 (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]`
```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/<id>/`)
**Файл:** `/home/busya/dev/superset/superset/views/core.py` (строки 771-831)
```python
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`
```python
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`
```python
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
```http
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": []
}
```
Ответ:
```json
{
"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
```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 <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 |