-
-
Notifications
You must be signed in to change notification settings - Fork 16.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Workflow #5665
Open
pgjones
wants to merge
2
commits into
main
Choose a base branch
from
workflow
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Workflow #5665
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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): # noqa: B024 | ||
"""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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This stuff isnt meant to be in the PR, right?