bito-code-review[bot] commented on code in PR #39469: URL: https://github.com/apache/superset/pull/39469#discussion_r3520040608
########## superset/utils/auth_session_stamp.py: ########## @@ -0,0 +1,392 @@ +# 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. +"""Session stamp: invalidate all browser sessions when a user's password changes.""" + +from __future__ import annotations + +import logging +import time +from typing import Any +from uuid import uuid4 + +from flask import Flask, has_request_context, session +from flask_login import current_user, logout_user +from sqlalchemy.exc import IntegrityError, SQLAlchemyError + +from superset.extensions import db +from superset.utils.decorators import transaction + +logger: logging.Logger = logging.getLogger(__name__) + +SESSION_AUTH_STAMP_SESSION_KEY = "_auth_session_stamp" +SESSION_AUTH_STAMP_VALIDATED_AT_KEY = "_auth_session_stamp_validated_at" +SESSION_AUTH_STAMP_VALIDATED_DB_STAMP_KEY = "_auth_session_stamp_validated_db_stamp" + +_SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"}) +_STAMP_CACHE_KEY_PREFIX = "auth_session_stamp:" +_DEFAULT_STAMP_CACHE_TIMEOUT_SECONDS = 300 + + +def _stamp_cache_key(user_id: int) -> str: + return f"{_STAMP_CACHE_KEY_PREFIX}{user_id}" + + +def _get_revalidation_seconds() -> int: + """Return ``SESSION_AUTH_STAMP_REVALIDATION_SECONDS``, defaulting on bad config.""" + from flask import current_app + + raw = current_app.config.get("SESSION_AUTH_STAMP_REVALIDATION_SECONDS", 0) + try: + return int(raw) + except (TypeError, ValueError): + logger.warning( + "Invalid SESSION_AUTH_STAMP_REVALIDATION_SECONDS=%r; " + "defaulting to 0 (check on every request)", + raw, + ) + return 0 + + +def _stamp_cache_timeout_seconds() -> int: + if (revalidation_seconds := _get_revalidation_seconds()) > 0: + return revalidation_seconds + return _DEFAULT_STAMP_CACHE_TIMEOUT_SECONDS + + +def _get_cached_user_session_stamp(user_id: int) -> str | None: + try: + from superset.extensions import cache_manager + + cached = cache_manager.cache.get(_stamp_cache_key(user_id)) + except Exception: # noqa: BLE001 + logger.debug( + "Unable to read session auth stamp cache for user_id=%s", + user_id, + exc_info=True, + ) + return None + return cached if isinstance(cached, str) else None + + +def _cache_user_session_stamp(user_id: int, stamp: str) -> None: + try: + from superset.extensions import cache_manager + + cache_manager.cache.set( + _stamp_cache_key(user_id), + stamp, + timeout=_stamp_cache_timeout_seconds(), + ) + except Exception: # noqa: BLE001 + logger.debug( + "Unable to write session auth stamp cache for user_id=%s", + user_id, + exc_info=True, + ) + + +def register_session_auth_stamp_hook(app: Flask) -> None: + """Register login and request hooks that manage the per-user session stamp.""" + if getattr(app, "superset_session_auth_stamp_hook_registered", False): + return + app.superset_session_auth_stamp_hook_registered = True + + from flask_login import user_logged_in + + if not getattr(app, "superset_session_auth_stamp_login_connected", False): + app.superset_session_auth_stamp_login_connected = True + + @user_logged_in.connect + def _sync_stamp_after_login(sender: Flask, user: Any, **extra: Any) -> None: # noqa: ARG001 + """Copy the DB stamp into the session after Flask-Login finalizes it.""" + sync_session_auth_stamp_on_login(user) + + @app.before_request + def _validate_user_session_auth_stamp() -> None: # noqa: WPS430 + """Log out requests whose session cookie carries an outdated auth stamp.""" + validate_session_auth_stamp_for_request() + + +@transaction() +def ensure_user_session_stamp_value(user_id: int) -> str: + """Return the stamp for ``user_id``, inserting a stable row if missing.""" + from superset.models.user_session_auth_stamp import UserSessionAuthStamp + + row = db.session.get(UserSessionAuthStamp, user_id) + if row is not None: + return row.stamp + stamp = str(uuid4()) + try: + with db.session.begin_nested(): + db.session.add(UserSessionAuthStamp(user_id=user_id, stamp=stamp)) + return stamp + except IntegrityError: + row = db.session.get(UserSessionAuthStamp, user_id) + if row is None: + logger.exception( + "Failed to resolve session auth stamp after IntegrityError " + "for user_id=%s", + user_id, + ) + raise + return row.stamp + + +def clear_flask_login_remember_cookie() -> None: + """ + Schedule deletion of any Flask-Login remember-me cookie on the HTTP response. + + Superset does not expose remember-me in the React login flow, but Flask-Login + and FAB still support persistent cookies when ``remember=True``. After a + password change, clear any existing remember token so it cannot + re-establish a session without re-authentication. + """ + if not has_request_context(): + return + session["_remember"] = "clear" + + +def sync_session_auth_stamp_on_login(user: Any) -> None: + """Copy the DB stamp into the signed session cookie after a successful login.""" + if not has_request_context(): + return + uid = getattr(user, "id", None) + if uid is None: + return + user_id = int(uid) + stamp = ensure_user_session_stamp_value(user_id) + session[SESSION_AUTH_STAMP_SESSION_KEY] = stamp + _mark_session_stamp_validated(stamp) + _cache_user_session_stamp(user_id, stamp) + + +def cache_user_session_auth_stamp(user_id: int, stamp: str) -> None: + """Write a validated session auth stamp into the shared cache.""" + _cache_user_session_stamp(user_id, stamp) Review Comment: <div> <div id="suggestion"> <div id="issue"><b>Missing tests for new public function</b></div> <div id="fix"> The new public function `cache_user_session_auth_stamp` introduced at line 177 is not covered by tests. Per rule [11730], comprehensive unit tests should be added for new tools covering success paths, error scenarios, and edge cases. Add the import to test_auth_session_stamp.py and verify the cache write behavior. </div> </div> <small><i>Code Review Run #5103d3</i></small> </div> --- Should Bito avoid suggestions like this for future reviews? (<a href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>) - [ ] Yes, avoid them -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
