Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
268c75c
chore: Bump FAB to 5.X
dpgaspar Apr 9, 2025
6c43dba
fix tests and remove appbuilder.app refs
dpgaspar Apr 9, 2025
377c140
fix tests
dpgaspar Apr 9, 2025
4361c68
bump to 5.0.0a6
dpgaspar Apr 13, 2025
37ee205
Merge branch 'master' into chore/fab-5.x
dpgaspar Apr 13, 2025
60af434
bump to 5.0.0a6
dpgaspar Apr 13, 2025
e7f345f
bump to 5.0.0a7
dpgaspar Apr 13, 2025
57bb783
merge master
dpgaspar Apr 22, 2025
918a261
re add manager.py
dpgaspar Apr 22, 2025
6af1c49
re add manager.py
dpgaspar Apr 22, 2025
00fdf5a
Merge remote-tracking branch 'origin/chore/fab-5.x' into chore/fab-5.x
dpgaspar Apr 22, 2025
dfdd5bc
re add manager.py
dpgaspar Apr 22, 2025
56f2ffe
re add manager.py
dpgaspar Apr 22, 2025
898d3d4
re add manager.py
dpgaspar Apr 22, 2025
85bcdfb
bump to 5.0.0a8
dpgaspar Apr 22, 2025
0ec5995
merge and fix conflicts
dpgaspar Aug 6, 2025
52c64e4
adapt to new FAB 5
dpgaspar Aug 6, 2025
0237711
fix deps
dpgaspar Aug 6, 2025
7d0af5c
fix deps
dpgaspar Aug 6, 2025
763319a
fix deps
dpgaspar Aug 6, 2025
69ae9ab
fix add __tablename__ dynamicplugin
dpgaspar Aug 6, 2025
3938241
use 5.0.0a10
dpgaspar Aug 6, 2025
443ac62
fix tests
dpgaspar Aug 6, 2025
baeabe8
fix tests
dpgaspar Aug 6, 2025
c61bba5
Merge branch 'master' into chore/fab-5.x
dpgaspar Aug 29, 2025
a50f5a5
fix
dpgaspar Aug 29, 2025
60e0df0
bump to 5.0.0a11
dpgaspar Sep 2, 2025
dc8c946
fix
dpgaspar Sep 2, 2025
3d46421
remove OIDC support
dpgaspar Sep 2, 2025
534354f
fix dynamic plugins
dpgaspar Sep 2, 2025
9068787
fix dynamic plugins
dpgaspar Sep 2, 2025
fa95d0d
Merge branch 'master' into chore/fab-5.x
dpgaspar Sep 5, 2025
fea39cc
test a12 without mongodb
dpgaspar Sep 5, 2025
b40b476
bump to final version
dpgaspar Sep 9, 2025
7621e8f
added updating line regarding OID
dpgaspar Sep 9, 2025
e5bc96c
fix lint
dpgaspar Sep 10, 2025
7b9113a
Merge branch 'master' into chore/fab-5.x
sadpandajoe Sep 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.

## Next
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
- [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files).
- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
Expand Down
104 changes: 0 additions & 104 deletions docs/docs/configuration/configuring-superset.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -363,110 +363,6 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
]
```

### Keycloak-Specific Configuration using Flask-OIDC

If you are using Keycloak as OpenID Connect 1.0 Provider, the above configuration based on [`Authlib`](https://authlib.org/) might not work. In this case using [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is a viable option.

Make sure the pip package [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is installed on the webserver. This was successfully tested using version 2.2.0. This package requires [`Flask-OpenID`](https://pypi.org/project/Flask-OpenID/) as a dependency.

The following code defines a new security manager. Add it to a new file named `keycloak_security_manager.py`, placed in the same directory as your `superset_config.py` file.

```python
from flask_appbuilder.security.manager import AUTH_OID
from superset.security import SupersetSecurityManager
from flask_oidc import OpenIDConnect
from flask_appbuilder.security.views import AuthOIDView
from flask_login import login_user
from urllib.parse import quote
from flask_appbuilder.views import ModelView, SimpleFormView, expose
from flask import (
redirect,
request
)
import logging

class OIDCSecurityManager(SupersetSecurityManager):

def __init__(self, appbuilder):
super(OIDCSecurityManager, self).__init__(appbuilder)
if self.auth_type == AUTH_OID:
self.oid = OpenIDConnect(self.appbuilder.get_app)
self.authoidview = AuthOIDCView

class AuthOIDCView(AuthOIDView):

@expose('/login/', methods=['GET', 'POST'])
def login(self, flag=True):
sm = self.appbuilder.sm
oidc = sm.oid

@self.appbuilder.sm.oid.require_login
def handle_login():
user = sm.auth_user_oid(oidc.user_getfield('email'))

if user is None:
info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email'])
user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'),
info.get('email'), sm.find_role('Gamma'))

login_user(user, remember=False)
return redirect(self.appbuilder.get_url_for_index)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OID is a legacy protocol.
This is not a secure implementation since it contains several security flaws, for example the JWT is not verified wish is required by the OIDC protocol. Also keycloak use should be possible using OAuth2.


return handle_login()

@expose('/logout/', methods=['GET', 'POST'])
def logout(self):
oidc = self.appbuilder.sm.oid

oidc.logout()
super(AuthOIDCView, self).logout()
redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login

return redirect(
oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url))
```

Then add to your `superset_config.py` file:

```python
from keycloak_security_manager import OIDCSecurityManager
from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH
import os

AUTH_TYPE = AUTH_OID
SECRET_KEY: 'SomethingNotEntirelySecret'
OIDC_CLIENT_SECRETS = '/path/to/client_secret.json'
OIDC_ID_TOKEN_COOKIE_SECURE = False
OIDC_OPENID_REALM: '<myRealm>'
OIDC_INTROSPECTION_AUTH_METHOD: 'client_secret_post'
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager

# Will allow user self registration, allowing to create Flask users from Authorized User
AUTH_USER_REGISTRATION = True

# The default user self registration role
AUTH_USER_REGISTRATION_ROLE = 'Public'
```

Store your client-specific OpenID information in a file called `client_secret.json`. Create this file in the same directory as `superset_config.py`:

```json
{
"<myOpenIDProvider>": {
"issuer": "https://<myKeycloakDomain>/realms/<myRealm>",
"auth_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/auth",
"client_id": "https://<myKeycloakDomain>",
"client_secret": "<myClientSecret>",
"redirect_uris": [
"https://<SupersetWebserver>/oauth-authorized/<myOpenIDProvider>"
],
"userinfo_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/userinfo",
"token_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token",
"token_introspection_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token/introspect"
}
}
```

## LDAP Authentication

FAB supports authenticating user credentials against an LDAP server.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,110 +363,6 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
]
```

### Keycloak-Specific Configuration using Flask-OIDC

If you are using Keycloak as OpenID Connect 1.0 Provider, the above configuration based on [`Authlib`](https://authlib.org/) might not work. In this case using [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is a viable option.

Make sure the pip package [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is installed on the webserver. This was successfully tested using version 2.2.0. This package requires [`Flask-OpenID`](https://pypi.org/project/Flask-OpenID/) as a dependency.

The following code defines a new security manager. Add it to a new file named `keycloak_security_manager.py`, placed in the same directory as your `superset_config.py` file.

```python
from flask_appbuilder.security.manager import AUTH_OID
from superset.security import SupersetSecurityManager
from flask_oidc import OpenIDConnect
from flask_appbuilder.security.views import AuthOIDView
from flask_login import login_user
from urllib.parse import quote
from flask_appbuilder.views import ModelView, SimpleFormView, expose
from flask import (
redirect,
request
)
import logging

class OIDCSecurityManager(SupersetSecurityManager):

def __init__(self, appbuilder):
super(OIDCSecurityManager, self).__init__(appbuilder)
if self.auth_type == AUTH_OID:
self.oid = OpenIDConnect(self.appbuilder.get_app)
self.authoidview = AuthOIDCView

class AuthOIDCView(AuthOIDView):

@expose('/login/', methods=['GET', 'POST'])
def login(self, flag=True):
sm = self.appbuilder.sm
oidc = sm.oid

@self.appbuilder.sm.oid.require_login
def handle_login():
user = sm.auth_user_oid(oidc.user_getfield('email'))

if user is None:
info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email'])
user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'),
info.get('email'), sm.find_role('Gamma'))

login_user(user, remember=False)
return redirect(self.appbuilder.get_url_for_index)

return handle_login()

@expose('/logout/', methods=['GET', 'POST'])
def logout(self):
oidc = self.appbuilder.sm.oid

oidc.logout()
super(AuthOIDCView, self).logout()
redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login

return redirect(
oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url))
```

Then add to your `superset_config.py` file:

```python
from keycloak_security_manager import OIDCSecurityManager
from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH
import os

AUTH_TYPE = AUTH_OID
SECRET_KEY: 'SomethingNotEntirelySecret'
OIDC_CLIENT_SECRETS = '/path/to/client_secret.json'
OIDC_ID_TOKEN_COOKIE_SECURE = False
OIDC_OPENID_REALM: '<myRealm>'
OIDC_INTROSPECTION_AUTH_METHOD: 'client_secret_post'
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager

# Will allow user self registration, allowing to create Flask users from Authorized User
AUTH_USER_REGISTRATION = True

# The default user self registration role
AUTH_USER_REGISTRATION_ROLE = 'Public'
```

Store your client-specific OpenID information in a file called `client_secret.json`. Create this file in the same directory as `superset_config.py`:

```json
{
"<myOpenIDProvider>": {
"issuer": "https://<myKeycloakDomain>/realms/<myRealm>",
"auth_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/auth",
"client_id": "https://<myKeycloakDomain>",
"client_secret": "<myClientSecret>",
"redirect_uris": [
"https://<SupersetWebserver>/oauth-authorized/<myOpenIDProvider>"
],
"userinfo_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/userinfo",
"token_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token",
"token_introspection_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token/introspect"
}
}
```

## LDAP Authentication

FAB supports authenticating user credentials against an LDAP server.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ dependencies = [
"cryptography>=42.0.4, <45.0.0",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <3.0.0",
"flask-appbuilder>=4.8.1, <5.0.0",
"flask-appbuilder>=5.0.0,<6",
"flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
Expand Down
8 changes: 3 additions & 5 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,9 @@ flask==2.3.3
# flask-session
# flask-sqlalchemy
# flask-wtf
flask-appbuilder==4.8.1
# via
# apache-superset (pyproject.toml)
# apache-superset-core
flask-babel==2.0.0
flask-appbuilder==5.0.0
# via apache-superset (pyproject.toml)
flask-babel==3.1.0
# via flask-appbuilder
flask-caching==2.3.1
# via apache-superset (pyproject.toml)
Expand Down
5 changes: 2 additions & 3 deletions requirements/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,11 @@ flask==2.3.3
# flask-sqlalchemy
# flask-testing
# flask-wtf
flask-appbuilder==4.8.1
flask-appbuilder==5.0.0
# via
# -c requirements/base-constraint.txt
# apache-superset
# apache-superset-core
flask-babel==2.0.0
flask-babel==3.1.0
# via
# -c requirements/base-constraint.txt
# flask-appbuilder
Expand Down
2 changes: 1 addition & 1 deletion superset-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"flask-appbuilder>=4.5.3, <5.0.0",
"flask-appbuilder>=5.0.0,<6",
]

[project.urls]
Expand Down
1 change: 0 additions & 1 deletion superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,6 @@ def _try_json_readsha(filepath: str, length: int) -> str | None:
# AUTHENTICATION CONFIG
# ----------------------------------------------------
# The authentication type
# AUTH_OID : Is for OpenID
# AUTH_DB : Is for database (username/password)
# AUTH_LDAP : Is for LDAP
# AUTH_REMOTE_USER : Is for using REMOTE_USER from web server
Expand Down
6 changes: 3 additions & 3 deletions superset/dashboards/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from typing import Any, Callable, cast
from zipfile import is_zipfile, ZipFile

from flask import g, redirect, request, Response, send_file, url_for
from flask import current_app, g, redirect, request, Response, send_file, url_for
from flask_appbuilder import permission_name
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
Expand Down Expand Up @@ -332,8 +332,8 @@ def __repr__(self) -> str:
"""Deterministic string representation of the API instance for etag_cache."""
# pylint: disable=consider-using-f-string
return "Superset.dashboards.api.DashboardRestApi@v{}{}".format(
self.appbuilder.app.config["VERSION_STRING"],
self.appbuilder.app.config["VERSION_SHA"],
current_app.config["VERSION_STRING"],
current_app.config["VERSION_SHA"],
)

@expose("/<id_or_slug>", methods=("GET",))
Expand Down
5 changes: 3 additions & 2 deletions superset/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@

import celery
from flask import Flask
from flask_appbuilder import AppBuilder, SQLA
from flask_appbuilder import AppBuilder
from flask_appbuilder.utils.legacy import get_sqla_class
from flask_caching.backends.base import BaseCache
from flask_migrate import Migrate
from flask_talisman import Talisman
Expand Down Expand Up @@ -123,7 +124,7 @@ def init_app(self, app: Flask) -> None:
cache_manager = CacheManager()
celery_app = celery.Celery()
csrf = CSRFProtect()
db = SQLA() # pylint: disable=disallowed-name
db = get_sqla_class()()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will allow compatibility for sqlalchemy 1.x and sqlalchemy 2.x at the same time

_event_logger: dict[str, Any] = {}
encrypted_field_factory = EncryptedFieldFactory()
event_logger = LocalProxy(lambda: _event_logger.get("event_logger"))
Expand Down
4 changes: 2 additions & 2 deletions superset/initialization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import wtforms_json
from colorama import Fore, Style
from deprecation import deprecated
from flask import abort, Flask, redirect, request, session, url_for
from flask import abort, current_app, Flask, redirect, request, session, url_for
from flask_appbuilder import expose, IndexView
from flask_appbuilder.api import safe
from flask_appbuilder.utils.base import get_safe_redirect
Expand Down Expand Up @@ -290,7 +290,7 @@ def init_views(self) -> None:
"Home",
label=_("Home"),
href="/superset/welcome/",
cond=lambda: bool(appbuilder.app.config["LOGO_TARGET_PATH"]),
cond=lambda: bool(current_app.config["LOGO_TARGET_PATH"]),
)

appbuilder.add_view(
Expand Down
4 changes: 2 additions & 2 deletions superset/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from alembic.operations.ops import MigrationScript
from alembic.runtime.migration import MigrationContext
from flask import current_app
from flask_appbuilder import Base
from flask_appbuilder import Model
from sqlalchemy import engine_from_config, pool

# this is the Alembic Config object, which provides
Expand All @@ -45,7 +45,7 @@
# Escape % chars in the database URI to avoid interpolation errors in ConfigParser
escaped_uri = DATABASE_URI.replace("%", "%%")
config.set_main_option("sqlalchemy.url", escaped_uri)
target_metadata = Base.metadata # pylint: disable=no-member
target_metadata = Model.metadata # pylint: disable=no-member


# other values from the config, defined by the needs of env.py,
Expand Down
1 change: 1 addition & 0 deletions superset/models/dynamic_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@


class DynamicPlugin(Model, AuditMixinNullable):
__tablename__ = "dynamic_plugin"
id = Column(Integer, primary_key=True)
name = Column(Text, unique=True, nullable=False)
# key corresponds to viz_type from static plugins
Expand Down
Loading
Loading