Skip to content

Commit 6e832f3

Browse files
feat(google cloud datastore): added google cloud datastore as a session backend
pallets-eco#109
1 parent e5dc958 commit 6e832f3

5 files changed

Lines changed: 515 additions & 2 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ Uses elasticsearch as a session backend. ([elasticsearch](https://elasticsearch-
162162
- SESSION_ELASTICSEARCH_HOST
163163
- SESSION_ELASTICSEARCH_INDEX
164164

165+
### `GoogleCloudDatastoreSessionInterface`
166+
167+
Uses Google Cloud Datastore as a session backend. ([google-cloud-datastore](https://github.com/googleapis/python-datastore) required)
168+
169+
- GCLOUD_APP_PROJECT_ID
170+
165171
## Credits
166172

167173
This project is a fork of [flask-session](https://github.com/fengsp/flask-session), created by [Shipeng Feng](https://github.com/fengsp).

flask_session/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .sessions import (
1616
ElasticsearchSessionInterface,
1717
FileSystemSessionInterface,
18+
GoogleCloudDatastoreSessionInterface,
1819
MemcachedSessionInterface,
1920
MongoDBSessionInterface,
2021
NullSessionInterface,
@@ -96,6 +97,7 @@ def _get_interface(self, app):
9697
config.setdefault("SESSION_SQLALCHEMY", None)
9798
config.setdefault("SESSION_SQLALCHEMY_TABLE", "sessions")
9899
config.setdefault("SESSION_SQLALCHEMY_SEQUENCE", None)
100+
config.setdefault("GCLOUD_APP_PROJECT_ID", "unknown")
99101

100102
if config["SESSION_TYPE"] == "redis":
101103
session_interface = RedisSessionInterface(
@@ -150,6 +152,13 @@ def _get_interface(self, app):
150152
config["SESSION_USE_SIGNER"],
151153
config["SESSION_PERMANENT"],
152154
)
155+
elif config["SESSION_TYPE"] == "datastore":
156+
session_interface = GoogleCloudDatastoreSessionInterface(
157+
config["GCLOUD_APP_PROJECT_ID"],
158+
config["SESSION_KEY_PREFIX"],
159+
config["SESSION_USE_SIGNER"],
160+
config["SESSION_PERMANENT"],
161+
)
153162
else:
154163
session_interface = NullSessionInterface()
155164

flask_session/sessions.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
:license: BSD, see LICENSE for more details.
99
"""
1010

11+
import os
1112
import sys
1213
import time
1314
from datetime import datetime
@@ -74,6 +75,10 @@ class SqlAlchemySession(ServerSideSession):
7475
pass
7576

7677

78+
class GoogleCloudDataStoreSession(ServerSideSession):
79+
pass
80+
81+
7782
class SessionInterface(FlaskSessionInterface):
7883
def _generate_sid(self):
7984
return str(uuid4())
@@ -800,3 +805,116 @@ def save_session(self, app, session, response):
800805
path=path,
801806
secure=secure,
802807
)
808+
809+
810+
class GoogleCloudDatastoreSessionInterface(SessionInterface):
811+
"""Uses the Google cloud datastore as a session backend.
812+
813+
:param key_prefix: A prefix that is added to all store keys.
814+
:param use_signer: Whether to sign the session id cookie or not.
815+
:param permanent: Whether to use permanent session or not.
816+
"""
817+
818+
serializer = pickle
819+
session_class = GoogleCloudDataStoreSession
820+
821+
def __init__(self, gcloud_project, key_prefix, use_signer=False, permanent=True):
822+
self.gcloud_project = gcloud_project
823+
self.key_prefix = key_prefix
824+
self.use_signer = use_signer
825+
self.permanent = permanent
826+
827+
def get_client(self):
828+
import requests
829+
from google.auth import compute_engine
830+
from google.cloud import datastore
831+
832+
if os.environ.get("DATASTORE_EMULATOR_HOST"):
833+
return datastore.Client(
834+
_http=requests.Session, project="virustotal-avs-control"
835+
)
836+
return datastore.Client(credentials=compute_engine.Credentials())
837+
838+
def open_session(self, app, request):
839+
ds_client = self.get_client()
840+
sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"])
841+
if not sid:
842+
sid = self._generate_sid()
843+
return self.session_class(sid=sid, permanent=self.permanent)
844+
if self.use_signer:
845+
signer = self._get_signer(app)
846+
if signer is None:
847+
return None
848+
try:
849+
sid_as_bytes = signer.unsign(sid)
850+
sid = sid_as_bytes.decode()
851+
except BadSignature:
852+
sid = self._generate_sid()
853+
return self.session_class(sid=sid, permanent=self.permanent)
854+
855+
store_id = self.key_prefix + sid
856+
session_key = ds_client.key("session", store_id)
857+
saved_session = ds_client.get(session_key)
858+
if saved_session and saved_session["expiry"] <= pytz.utc.localize(
859+
datetime.now()
860+
):
861+
ds_client.delete(session_key)
862+
saved_session = None
863+
if saved_session:
864+
try:
865+
value = saved_session["data"]
866+
data = self.serializer.loads(want_bytes(value))
867+
return self.session_class(data, sid=sid)
868+
except:
869+
return self.session_class(sid=sid, permanent=self.permanent)
870+
return self.session_class(sid=sid, permanent=self.permanent)
871+
872+
def save_session(self, app, session, response):
873+
from google.cloud import datastore
874+
875+
ds_client = self.get_client()
876+
domain = self.get_cookie_domain(app)
877+
path = self.get_cookie_path(app)
878+
store_id = self.key_prefix + session.sid
879+
session_key = ds_client.key("session", store_id)
880+
saved_session = ds_client.get(session_key)
881+
if not session:
882+
if session.modified:
883+
if saved_session:
884+
ds_client.delete(session_key)
885+
response.delete_cookie(
886+
app.config["SESSION_COOKIE_NAME"], domain=domain, path=path
887+
)
888+
return
889+
890+
httponly = self.get_cookie_httponly(app)
891+
secure = self.get_cookie_secure(app)
892+
expires = self.get_expiration_time(app, session)
893+
value = self.serializer.dumps(dict(session))
894+
if saved_session:
895+
if not expires:
896+
ds_client.delete(session_key)
897+
return
898+
saved_session["data"] = value
899+
saved_session["expiry"] = expires
900+
ds_client.put(saved_session)
901+
else:
902+
new_session = datastore.Entity(
903+
key=session_key, exclude_from_indexes=("data",)
904+
)
905+
new_session["data"] = value
906+
new_session["expiry"] = expires
907+
ds_client.put(new_session)
908+
if self.use_signer:
909+
session_id = self._get_signer(app).sign(want_bytes(session.sid))
910+
else:
911+
session_id = session.sid
912+
response.set_cookie(
913+
app.config["SESSION_COOKIE_NAME"],
914+
session_id,
915+
expires=expires,
916+
httponly=httponly,
917+
domain=domain,
918+
path=path,
919+
secure=secure,
920+
)

0 commit comments

Comments
 (0)