From 857fa041ad6f0f1b737d17422f20f3e61b355d84 Mon Sep 17 00:00:00 2001 From: Maurice Borgmeier Date: Fri, 12 Apr 2024 13:37:28 +0200 Subject: [PATCH 1/4] Add table_exists parameter and extend documentation, fix typo in CONTRIBUTING.rst --- CONTRIBUTING.rst | 4 +- CONTRIBUTORS.md | 1 + docs/config_reference.rst | 7 +++ src/flask_session/__init__.py | 4 ++ src/flask_session/defaults.py | 1 + src/flask_session/dynamodb/dynamodb.py | 81 ++++++++++++++++++++------ tests/test_dynamodb.py | 41 +++++++++++++ 7 files changed, 119 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8793b138..75559ee6 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -16,7 +16,7 @@ Install dependencies .. code-block:: bash - $ pip install -r requirements/dev.in + $ pip install -r requirements/dev.txt $ pip install -r requirements/docs.in Install the package in editable mode @@ -44,7 +44,7 @@ or $ sphinx-build -b html docs docs/_build -Run the tests together or individually +Run the tests together or individually, requires the docker containers to be up and running (see below) .. code-block:: bash diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 62186cc4..d51a49ec 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,5 +1,6 @@ ## Contributors +- [MauriceBrg](https://github.com/MauriceBrg) - [eiriklid](https://github.com/eiriklid) - [necat1](https://github.com/necat1) - [nebolax](https://github.com/nebolax) diff --git a/docs/config_reference.rst b/docs/config_reference.rst index e3ace977..1ebe4b14 100644 --- a/docs/config_reference.rst +++ b/docs/config_reference.rst @@ -18,6 +18,7 @@ These are specific to Flask-Session. - **cachelib**: CacheLibSessionInterface - **mongodb**: MongoDBSessionInterface - **sqlalchemy**: SqlAlchemySessionInterface + - **dynamodb**: DynamoDBSessionInterface .. py:data:: SESSION_PERMANENT @@ -215,6 +216,12 @@ Dynamodb Default: ``'Sessions'`` +.. py:data:: SESSION_DYNAMODB_TABLE_NAME_EXISTS + + By default it will create a new table with the TTL setting activated unless you set this parameter to ``True``, then it assumes that the table already exists. + + Default: ``False`` + .. deprecated:: 0.7.0 ``SESSION_FILE_DIR``, ``SESSION_FILE_THRESHOLD``, ``SESSION_FILE_MODE``. Use ``SESSION_CACHELIB`` instead. diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index b95a0b05..679cbdf6 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -109,6 +109,9 @@ def _get_interface(self, app): SESSION_DYNAMODB_TABLE = config.get( "SESSION_DYNAMODB_TABLE", Defaults.SESSION_DYNAMODB_TABLE ) + SESSION_DYNAMODB_TABLE_EXISTS = config.get( + "SESSION_DYNAMODB_TABLE_EXISTS", Defaults.SESSION_DYNAMODB_TABLE_EXISTS + ) common_params = { "app": app, @@ -178,6 +181,7 @@ def _get_interface(self, app): **common_params, client=SESSION_DYNAMODB, table_name=SESSION_DYNAMODB_TABLE, + table_exists=SESSION_DYNAMODB_TABLE_EXISTS, ) else: diff --git a/src/flask_session/defaults.py b/src/flask_session/defaults.py index 7f890d6e..a21ffcbf 100644 --- a/src/flask_session/defaults.py +++ b/src/flask_session/defaults.py @@ -43,3 +43,4 @@ class Defaults: # DynamoDB settings SESSION_DYNAMODB = None SESSION_DYNAMODB_TABLE = "Sessions" + SESSION_DYNAMODB_TABLE_EXISTS = False diff --git a/src/flask_session/dynamodb/dynamodb.py b/src/flask_session/dynamodb/dynamodb.py index a9c448ec..9017dfdb 100644 --- a/src/flask_session/dynamodb/dynamodb.py +++ b/src/flask_session/dynamodb/dynamodb.py @@ -1,3 +1,5 @@ +"""Provides a Session Interface to DynamoDB""" + import warnings from datetime import datetime from datetime import timedelta as TimeDelta @@ -5,9 +7,9 @@ from typing import Optional import boto3 -from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource from flask import Flask from itsdangerous import want_bytes +from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource from ..base import ServerSideSession, ServerSideSessionInterface from ..defaults import Defaults @@ -20,12 +22,41 @@ class DynamoDBSession(ServerSideSession): class DynamoDBSessionInterface(ServerSideSessionInterface): """A Session interface that uses dynamodb as backend. (`boto3` required) - :param client: A ``DynamoDBServiceResource`` instance. + By default (``table_exists=False``) it will create a DynamoDB table with this configuration: + + - Table Name: Value of ``table_name``, by default ``Sessions`` + - Key Schema: Simple Primary Key ``id`` of type string + - Billing Mode: Pay per Request + - Time to Live enabled, attribute name: ``expiration`` + - The following permissions are required: + - ``dynamodb:CreateTable`` + - ``dynamodb:DescribeTable`` + - ``dynamodb:UpdateTimeToLive`` + - ``dynamodb:GetItem`` + - ``dynamodb:UpdateItem`` + - ``dynamodb:DeleteItem`` + + If you set ``table_exists`` to True, you're responsible for creating a table with this config: + + - Table Name: Value of ``table_name``, by default ``Sessions`` + - Key Schema: Simple Primary Key ``id`` of type string + - Time to Live enabled, attribute name: ``expiration`` + - The following permissions are required under these circumstances: + - ``dynamodb:GetItem`` + - ``dynamodb:UpdateItem`` + - ``dynamodb:DeleteItem`` + + :param client: A ``DynamoDBServiceResource`` instance, i.e. the result + of ``boto3.resource("dynamodb", ...)``. :param key_prefix: A prefix that is added to all DynamoDB store keys. :param use_signer: Whether to sign the session id cookie or not. :param permanent: Whether to use permanent session or not. :param sid_length: The length of the generated session id in bytes. :param table_name: DynamoDB table name to store the session. + :param table_exists: The table already exists, don't try to create it (default=False). + + .. versionadded:: 0.9 + The `table_exists` parameter was added. .. versionadded:: 0.6 The `sid_length` parameter was added. @@ -46,8 +77,11 @@ def __init__( sid_length: int = Defaults.SESSION_ID_LENGTH, serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT, table_name: str = Defaults.SESSION_DYNAMODB_TABLE, + table_exists: Optional[bool] = Defaults.SESSION_DYNAMODB_TABLE_EXISTS, ): + # NOTE: The name client is a bit misleading as we're using the resource API of boto3 as opposed to the service API + # which would be instantiated as boto3.client. if client is None: warnings.warn( "No valid DynamoDBServiceResource instance provided, attempting to create a new instance on localhost:8000.", @@ -62,40 +96,51 @@ def __init__( aws_secret_access_key="dummy", ) + self.client = client + self.table_name = table_name + + if not table_exists: + self._create_table() + + self.store = client.Table(table_name) + super().__init__( + app, + key_prefix, + use_signer, + permanent, + sid_length, + serialization_format, + ) + + def _create_table(self): try: - client.create_table( + self.client.create_table( AttributeDefinitions=[ {"AttributeName": "id", "AttributeType": "S"}, ], - TableName=table_name, + TableName=self.table_name, KeySchema=[ {"AttributeName": "id", "KeyType": "HASH"}, ], BillingMode="PAY_PER_REQUEST", ) - client.meta.client.get_waiter("table_exists").wait(TableName=table_name) - client.meta.client.update_time_to_live( + self.client.meta.client.get_waiter("table_exists").wait( + TableName=self.table_name + ) + self.client.meta.client.update_time_to_live( TableName=self.table_name, TimeToLiveSpecification={ "Enabled": True, "AttributeName": "expiration", }, ) - except (AttributeError, client.meta.client.exceptions.ResourceInUseException): + except ( + AttributeError, + self.client.meta.client.exceptions.ResourceInUseException, + ): # TTL already exists, or table already exists pass - self.client = client - self.store = client.Table(table_name) - super().__init__( - app, - key_prefix, - use_signer, - permanent, - sid_length, - serialization_format, - ) - def _retrieve_session_data(self, store_id: str) -> Optional[dict]: # Get the saved session (document) from the database document = self.store.get_item(Key={"id": store_id}).get("Item") diff --git a/tests/test_dynamodb.py b/tests/test_dynamodb.py index a0e1f1fb..0168a53c 100644 --- a/tests/test_dynamodb.py +++ b/tests/test_dynamodb.py @@ -2,6 +2,7 @@ import boto3 import flask +import pytest from flask_session.defaults import Defaults from flask_session.dynamodb import DynamoDBSession @@ -52,3 +53,43 @@ def test_dynamodb_default(self, app_utils): with app.test_request_context(): assert isinstance(flask.session, DynamoDBSession) app_utils.test_session(app) + + def test_dynamodb_with_existing_table(self, app_utils): + """ + Setting the SESSION_DYNAMODB_TABLE_EXISTS to True for an + existing table shouldn't change anything. + """ + + with self.setup_dynamodb(): + app = app_utils.create_app( + { + "SESSION_TYPE": "dynamodb", + "SESSION_DYNAMODB": self.client, + "SESSION_DYNAMODB_TABLE_EXISTS": True, + } + ) + + with app.test_request_context(): + assert isinstance(flask.session, DynamoDBSession) + app_utils.test_session(app) + + def test_dynamodb_with_existing_table_fails_if_table_doesnt_exist(self, app_utils): + """Accessing a non-existent table should result in problems.""" + + app = app_utils.create_app( + { + "SESSION_TYPE": "dynamodb", + "SESSION_DYNAMODB": boto3.resource( + "dynamodb", + endpoint_url="http://localhost:8000", + region_name="us-west-2", + aws_access_key_id="dummy", + aws_secret_access_key="dummy", + ), + "SESSION_DYNAMODB_TABLE": "non-existent-123", + "SESSION_DYNAMODB_TABLE_EXISTS": True, + } + ) + with app.test_request_context(), pytest.raises(AssertionError): + assert isinstance(flask.session, DynamoDBSession) + app_utils.test_session(app) From 85eb3339434f775d34381840834ea44fe93a62d0 Mon Sep 17 00:00:00 2001 From: Maurice Borgmeier Date: Fri, 12 Apr 2024 13:51:01 +0200 Subject: [PATCH 2/4] Fix title underline to short warning --- docs/config_serialization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config_serialization.rst b/docs/config_serialization.rst index fc3f5325..f393b554 100644 --- a/docs/config_serialization.rst +++ b/docs/config_serialization.rst @@ -14,7 +14,7 @@ The msgspec library has speed and memory advantages over other libraries. Howeve If you encounter a TypeError such as: "Encoding objects of type is unsupported", you may be attempting to serialize an unsupported type. In this case, you can either convert the object to a supported type or use a different serializer. Casting to a supported type: -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python From f66484b51ec826fcc8707ba69cb5dcf68a340d23 Mon Sep 17 00:00:00 2001 From: Maurice Borgmeier Date: Thu, 18 Apr 2024 08:50:50 +0200 Subject: [PATCH 3/4] Fixes pallets-eco/flask-session/issues/240 --- src/flask_session/dynamodb/dynamodb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/flask_session/dynamodb/dynamodb.py b/src/flask_session/dynamodb/dynamodb.py index 9017dfdb..a7d4e3a0 100644 --- a/src/flask_session/dynamodb/dynamodb.py +++ b/src/flask_session/dynamodb/dynamodb.py @@ -144,7 +144,10 @@ def _create_table(self): def _retrieve_session_data(self, store_id: str) -> Optional[dict]: # Get the saved session (document) from the database document = self.store.get_item(Key={"id": store_id}).get("Item") - if document: + session_is_not_expired = Decimal(datetime.utcnow().timestamp()) <= document.get( + "expiration" + ) + if document and session_is_not_expired: serialized_session_data = want_bytes(document.get("val").value) return self.serializer.loads(serialized_session_data) return None From 0001bb3d577c61315c095ed447f7d8749a393447 Mon Sep 17 00:00:00 2001 From: Maurice Borgmeier Date: Thu, 18 Apr 2024 09:09:08 +0200 Subject: [PATCH 4/4] Fix typo in parameter reference in the docs --- docs/config_reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config_reference.rst b/docs/config_reference.rst index 1ebe4b14..9fef8fd5 100644 --- a/docs/config_reference.rst +++ b/docs/config_reference.rst @@ -216,7 +216,7 @@ Dynamodb Default: ``'Sessions'`` -.. py:data:: SESSION_DYNAMODB_TABLE_NAME_EXISTS +.. py:data:: SESSION_DYNAMODB_TABLE_EXISTS By default it will create a new table with the TTL setting activated unless you set this parameter to ``True``, then it assumes that the table already exists.