From d2f511abba5240c137405267e0ebe30b9e3504d4 Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Tue, 31 Oct 2023 16:05:18 +0000 Subject: [PATCH] feat: support server-side sessions (#25795) --- docs/docs/security/security.mdx | 34 +++++++++++++++++++++++++++-- requirements/base.txt | 7 +++++- requirements/testing.txt | 3 ++- setup.py | 1 + superset/config.py | 12 ++++++++++ superset/initialization/__init__.py | 6 +++++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/docs/docs/security/security.mdx b/docs/docs/security/security.mdx index b92890272..b3a4ae09c 100644 --- a/docs/docs/security/security.mdx +++ b/docs/docs/security/security.mdx @@ -4,7 +4,7 @@ hide_title: true sidebar_position: 1 --- -Security in Superset is handled by Flask AppBuilder (FAB), an application development framework +Authentication and authorization in Superset is handled by Flask AppBuilder (FAB), an application development framework built on top of Flask. FAB provides authentication, user management, permissions and roles. Please read its [Security documentation](https://flask-appbuilder.readthedocs.io/en/latest/security.html). @@ -67,7 +67,9 @@ objects (dashboards and slices) associated with the tables you just extended the ### REST API for user & role management -Flask-AppBuilder supports a REST API for user CRUD, but this feature is in beta and is not enabled by default in Superset. To enable this feature, set the following in your Superset configuration: +Flask-AppBuilder supports a REST API for user CRUD, +but this feature is in beta and is not enabled by default in Superset. +To enable this feature, set the following in your Superset configuration: ```python FAB_ADD_SECURITY_API = True @@ -165,6 +167,34 @@ HTTPS if the cookie is marked “secure”. The application must be served over `PERMANENT_SESSION_LIFETIME`: (default: "31 days") The lifetime of a permanent session as a `datetime.timedelta` object. +#### Switching to server side sessions + +Server side sessions offer benefits over client side sessions on security and performance. +By enabling server side sessions, the session data is stored server side and only a session ID +is sent to the client. When a user logs in, a session is created server side and the session ID +is sent to the client in a cookie. The client will send the session ID with each request and the +server will use it to retrieve the session data. +On logout, the session is destroyed server side and the session cookie is deleted on the client side. +This reduces the risk for replay attacks and session hijacking. + +Superset uses [Flask-Session](https://flask-session.readthedocs.io/en/latest/) to manage server side sessions. +To enable this extension you have to set: + +``` python +SESSION_SERVER_SIDE = True +``` + +Flask-Session offers multiple backend session interfaces for Flask, here's an example for Redis: + +``` python +from redis import Redis + +SESSION_TYPE = "redis" +SESSION_REDIS = Redis(host="redis", port=6379, db=0) +# sign the session cookie sid +SESSION_USE_SIGNER = True +``` + ### Content Security Policy (CSP) Superset uses the [Talisman](https://pypi.org/project/flask-talisman/) extension to enable implementation of a diff --git a/requirements/base.txt b/requirements/base.txt index 5734f68d7..d056b403c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -32,7 +32,9 @@ bottleneck==1.3.7 brotli==1.0.9 # via flask-compress cachelib==0.6.0 - # via flask-caching + # via + # flask-caching + # flask-session celery==5.2.2 # via apache-superset certifi==2023.7.22 @@ -94,6 +96,7 @@ flask==2.2.5 # flask-limiter # flask-login # flask-migrate + # flask-session # flask-sqlalchemy # flask-wtf flask-appbuilder==4.3.9 @@ -114,6 +117,8 @@ flask-login==0.6.0 # flask-appbuilder flask-migrate==3.1.0 # via apache-superset +flask-session==0.5.0 + # via apache-superset flask-sqlalchemy==2.5.1 # via # flask-appbuilder diff --git a/requirements/testing.txt b/requirements/testing.txt index d3cfde521..00fe73454 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -48,7 +48,6 @@ google-auth==2.17.3 # google-cloud-core # pandas-gbq # pydata-google-auth - # shillelagh # sqlalchemy-bigquery google-auth-oauthlib==1.0.0 # via @@ -120,6 +119,8 @@ protobuf==4.23.0 # proto-plus pydata-google-auth==1.7.0 # via pandas-gbq +pyee==9.0.4 + # via playwright pyfakefs==5.2.2 # via -r requirements/testing.in pyhive[presto]==0.7.0 diff --git a/setup.py b/setup.py index a5bd4347f..2ea20c29a 100644 --- a/setup.py +++ b/setup.py @@ -90,6 +90,7 @@ setup( "flask-talisman>=1.0.0, <2.0", "flask-login>=0.6.0, < 1.0", "flask-migrate>=3.1.0, <4.0", + "flask-session>=0.4.0, <1.0", "flask-wtf>=1.1.0, <2.0", "func_timeout", "geopy", diff --git a/superset/config.py b/superset/config.py index dd244dc14..a85cbe82e 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1482,6 +1482,18 @@ TALISMAN_DEV_CONFIG = { SESSION_COOKIE_HTTPONLY = True # Prevent cookie from being read by frontend JS? SESSION_COOKIE_SECURE = False # Prevent cookie from being transmitted over non-tls? SESSION_COOKIE_SAMESITE: Literal["None", "Lax", "Strict"] | None = "Lax" +# Whether to use server side sessions from flask-session or Flask secure cookies +SESSION_SERVER_SIDE = False +# Example config using Redis as the backend for server side sessions +# from flask_session import RedisSessionInterface +# +# SESSION_SERVER_SIDE = True +# SESSION_USE_SIGNER = True +# SESSION_TYPE = "redis" +# SESSION_REDIS = Redis(host="localhost", port=6379, db=0) +# +# Other possible config options and backends: +# # https://flask-session.readthedocs.io/en/latest/config.html # Cache static resources. SEND_FILE_MAX_AGE_DEFAULT = int(timedelta(days=365).total_seconds()) diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index e84689994..09212120e 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -28,6 +28,7 @@ from flask import Flask, redirect from flask_appbuilder import expose, IndexView from flask_babel import gettext as __ from flask_compress import Compress +from flask_session import Session from werkzeug.middleware.proxy_fix import ProxyFix from superset.constants import CHANGE_ME_SECRET_KEY @@ -479,6 +480,10 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods logger.error("Refusing to start due to insecure SECRET_KEY") sys.exit(1) + def configure_session(self) -> None: + if self.config["SESSION_SERVER_SIDE"]: + Session(self.superset_app) + def init_app(self) -> None: """ Main entry point which will delegate to other methods in @@ -486,6 +491,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods """ self.pre_init() self.check_secret_key() + self.configure_session() # Configuration of logging must be done first to apply the formatter properly self.configure_logging() # Configuration of feature_flags must be done first to allow init features