# 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/<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 (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/<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 |
