Source code for airflow.providers.fab.www.extensions.init_session
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import logging
import time
from flask import session as builtin_flask_session
from flask_login import current_user, logout_user, user_logged_in
from airflow.exceptions import AirflowConfigException
from airflow.providers.common.compat.sdk import conf
from airflow.providers.fab.www.session import (
AirflowDatabaseSessionInterface,
AirflowSecureCookieSessionInterface,
)
[docs]
log = logging.getLogger(__name__)
[docs]
SESSION_LOGIN_TIME_KEY = "_login_at"
[docs]
def init_airflow_session_interface(app, db):
"""Set airflow session interface."""
config = app.config.copy()
selected_backend = conf.get("fab", "SESSION_BACKEND")
# A bit of a misnomer - normally cookies expire whenever the browser is closed
# or when they hit their expiry datetime, whichever comes first. "Permanent"
# cookies only expire when they hit their expiry datetime, and can outlive
# the browser being closed.
permanent_cookie = config.get("SESSION_PERMANENT", True)
if selected_backend == "securecookie":
app.session_interface = AirflowSecureCookieSessionInterface()
if permanent_cookie:
def make_session_permanent():
builtin_flask_session.permanent = True
app.before_request(make_session_permanent)
elif selected_backend == "database":
app.session_interface = AirflowDatabaseSessionInterface(
app=app,
client=db,
permanent=permanent_cookie,
# Typically these would be configurable with Flask-Session,
# but we will set them explicitly instead as they don't make
# sense to have configurable in Airflow's use case
table="session",
key_prefix="",
use_signer=True,
)
else:
raise AirflowConfigException(
"Unrecognized session backend specified in "
f"[fab] session_backend: '{selected_backend}'. Please set "
"this to either 'database' or 'securecookie'."
)
[docs]
def get_max_session_lifetime_seconds() -> int:
"""Return ``[fab] session_max_lifetime_minutes`` in seconds, or ``0`` when the cap is disabled."""
return max(conf.getint("fab", "session_max_lifetime_minutes", fallback=0), 0) * 60
[docs]
def get_remaining_session_lifetime() -> float | None:
"""
Return how many seconds are left before the current session hits its maximum lifetime.
``None`` means the session is not capped, either because ``[fab]
session_max_lifetime_minutes`` is disabled or because the session carries no login stamp.
"""
max_lifetime_seconds = get_max_session_lifetime_seconds()
if not max_lifetime_seconds:
return None
login_time = builtin_flask_session.get(SESSION_LOGIN_TIME_KEY)
if login_time is None:
return None
return login_time + max_lifetime_seconds - time.time()
[docs]
def init_session_max_lifetime(app):
"""Expire sessions ``[fab] session_max_lifetime_minutes`` after login, regardless of activity."""
if not get_max_session_lifetime_seconds():
return
# ``weak=False``: the receiver is a local function, so a weak subscription could be collected.
@user_logged_in.connect_via(app, weak=False)
def stamp_login_time(sender, user, **kwargs):
# Wall clock rather than ``time.monotonic()``: the stamp is persisted in the session and
# read back by other API server processes, which share no monotonic clock origin.
builtin_flask_session[SESSION_LOGIN_TIME_KEY] = time.time()
@app.before_request
def expire_session_past_max_lifetime():
if SESSION_LOGIN_TIME_KEY not in builtin_flask_session:
# Sessions that predate this setting have no stamp; cap them from now on rather than
# leaving them exempt forever.
if current_user.is_authenticated:
builtin_flask_session[SESSION_LOGIN_TIME_KEY] = time.time()
return
remaining = get_remaining_session_lifetime()
if remaining is not None and remaining < 0:
log.debug("Session reached [fab] session_max_lifetime_minutes, expiring it.")
# ``logout_user`` rather than emptying the session: it also invalidates the
# remember-me cookie and drops the user from the request context, so the request that
# crossed the deadline is itself unauthenticated.
logout_user()
builtin_flask_session.pop(SESSION_LOGIN_TIME_KEY, None)