- 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
18 KiB
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—LoginPostschema/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 — параметры
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)
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:
- Получить guest token через REST API (требует обычный JWT)
- Открыть
/embedded/<uuid>(анонимная сессия) - 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 = TrueGUEST_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 |