-
-
Notifications
You must be signed in to change notification settings - Fork 16.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move parts of the session code to sansio
This will allow it to be used in Quart, thereby reducing duplication, and ensuring the APIs match.
- Loading branch information
Showing
2 changed files
with
225 additions
and
203 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
from __future__ import annotations | ||
|
||
import collections.abc as c | ||
import typing as t | ||
from abc import ABCMeta | ||
from collections.abc import MutableMapping | ||
from datetime import datetime | ||
from datetime import timezone | ||
|
||
from werkzeug.datastructures import CallbackDict | ||
|
||
from .app import App | ||
|
||
if t.TYPE_CHECKING: # pragma: no cover | ||
import typing_extensions as te | ||
|
||
|
||
class SessionMixin(MutableMapping[str, t.Any]): | ||
"""Expands a basic dictionary with session attributes.""" | ||
|
||
@property | ||
def permanent(self) -> bool: | ||
"""This reflects the ``'_permanent'`` key in the dict.""" | ||
return self.get("_permanent", False) | ||
|
||
@permanent.setter | ||
def permanent(self, value: bool) -> None: | ||
self["_permanent"] = bool(value) | ||
|
||
#: Some implementations can detect whether a session is newly | ||
#: created, but that is not guaranteed. Use with caution. The mixin | ||
# default is hard-coded ``False``. | ||
new = False | ||
|
||
#: Some implementations can detect changes to the session and set | ||
#: this when that happens. The mixin default is hard coded to | ||
#: ``True``. | ||
modified = True | ||
|
||
#: Some implementations can detect when session data is read or | ||
#: written and set this when that happens. The mixin default is hard | ||
#: coded to ``True``. | ||
accessed = True | ||
|
||
|
||
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): | ||
"""Base class for sessions based on signed cookies. | ||
This session backend will set the :attr:`modified` and | ||
:attr:`accessed` attributes. It cannot reliably track whether a | ||
session is new (vs. empty), so :attr:`new` remains hard coded to | ||
``False``. | ||
""" | ||
|
||
#: When data is changed, this is set to ``True``. Only the session | ||
#: dictionary itself is tracked; if the session contains mutable | ||
#: data (for example a nested dict) then this must be set to | ||
#: ``True`` manually when modifying that data. The session cookie | ||
#: will only be written to the response if this is ``True``. | ||
modified = False | ||
|
||
#: When data is read or written, this is set to ``True``. Used by | ||
# :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` | ||
#: header, which allows caching proxies to cache different pages for | ||
#: different users. | ||
accessed = False | ||
|
||
def __init__( | ||
self, | ||
initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, | ||
) -> None: | ||
def on_update(self: te.Self) -> None: | ||
self.modified = True | ||
self.accessed = True | ||
|
||
super().__init__(initial, on_update) | ||
|
||
def __getitem__(self, key: str) -> t.Any: | ||
self.accessed = True | ||
return super().__getitem__(key) | ||
|
||
def get(self, key: str, default: t.Any = None) -> t.Any: | ||
self.accessed = True | ||
return super().get(key, default) | ||
|
||
def setdefault(self, key: str, default: t.Any = None) -> t.Any: | ||
self.accessed = True | ||
return super().setdefault(key, default) | ||
|
||
|
||
class NullSession(SecureCookieSession): | ||
"""Class used to generate nicer error messages if sessions are not | ||
available. Will still allow read-only access to the empty session | ||
but fail on setting. | ||
""" | ||
|
||
def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: | ||
raise RuntimeError( | ||
"The session is unavailable because no secret " | ||
"key was set. Set the secret_key on the " | ||
"application to something unique and secret." | ||
) | ||
|
||
__setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 | ||
del _fail | ||
|
||
|
||
class SessionInterface(metaclass=ABCMeta): | ||
"""This is a SansIO abstract base class used by Flask and Quart to | ||
then define thebasic interface to implement in order to replace | ||
the default session interface of the frameworks. | ||
.. versionadded:: 3.2.0 | ||
""" | ||
|
||
#: :meth:`make_null_session` will look here for the class that should | ||
#: be created when a null session is requested. Likewise the | ||
#: :meth:`is_null_session` method will perform a typecheck against | ||
#: this type. | ||
null_session_class = NullSession | ||
|
||
#: A flag that indicates if the session interface is pickle based. | ||
#: This can be used by Flask extensions to make a decision in regards | ||
#: to how to deal with the session object. | ||
#: | ||
#: .. versionadded:: 0.10 | ||
pickle_based = False | ||
|
||
def is_null_session(self, obj: object) -> bool: | ||
"""Checks if a given object is a null session. Null sessions are | ||
not asked to be saved. | ||
This checks if the object is an instance of :attr:`null_session_class` | ||
by default. | ||
""" | ||
return isinstance(obj, self.null_session_class) | ||
|
||
def get_cookie_name(self, app: App) -> str: | ||
"""The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" | ||
return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] | ||
|
||
def get_cookie_domain(self, app: App) -> str | None: | ||
"""The value of the ``Domain`` parameter on the session cookie. If not set, | ||
browsers will only send the cookie to the exact domain it was set from. | ||
Otherwise, they will send it to any subdomain of the given value as well. | ||
Uses the :data:`SESSION_COOKIE_DOMAIN` config. | ||
.. versionchanged:: 2.3 | ||
Not set by default, does not fall back to ``SERVER_NAME``. | ||
""" | ||
return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] | ||
|
||
def get_cookie_path(self, app: App) -> str: | ||
"""Returns the path for which the cookie should be valid. The | ||
default implementation uses the value from the ``SESSION_COOKIE_PATH`` | ||
config var if it's set, and falls back to ``APPLICATION_ROOT`` or | ||
uses ``/`` if it's ``None``. | ||
""" | ||
return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] | ||
|
||
def get_cookie_httponly(self, app: App) -> bool: | ||
"""Returns True if the session cookie should be httponly. This | ||
currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` | ||
config var. | ||
""" | ||
return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] | ||
|
||
def get_cookie_secure(self, app: App) -> bool: | ||
"""Returns True if the cookie should be secure. This currently | ||
just returns the value of the ``SESSION_COOKIE_SECURE`` setting. | ||
""" | ||
return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] | ||
|
||
def get_cookie_samesite(self, app: App) -> str | None: | ||
"""Return ``'Strict'`` or ``'Lax'`` if the cookie should use the | ||
``SameSite`` attribute. This currently just returns the value of | ||
the :data:`SESSION_COOKIE_SAMESITE` setting. | ||
""" | ||
return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] | ||
|
||
def get_cookie_partitioned(self, app: App) -> bool: | ||
"""Returns True if the cookie should be partitioned. By default, uses | ||
the value of :data:`SESSION_COOKIE_PARTITIONED`. | ||
.. versionadded:: 3.1 | ||
""" | ||
return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return] | ||
|
||
def get_expiration_time(self, app: App, session: SessionMixin) -> datetime | None: | ||
"""A helper method that returns an expiration date for the session | ||
or ``None`` if the session is linked to the browser session. The | ||
default implementation returns now + the permanent session | ||
lifetime configured on the application. | ||
""" | ||
if session.permanent: | ||
return datetime.now(timezone.utc) + app.permanent_session_lifetime | ||
return None | ||
|
||
def should_set_cookie(self, app: App, session: SessionMixin) -> bool: | ||
"""Used by session backends to determine if a ``Set-Cookie`` header | ||
should be set for this session cookie for this response. If the session | ||
has been modified, the cookie is set. If the session is permanent and | ||
the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is | ||
always set. | ||
This check is usually skipped if the session was deleted. | ||
.. versionadded:: 0.11 | ||
""" | ||
|
||
return session.modified or ( | ||
session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] | ||
) |
Oops, something went wrong.