This is an automated email from the ASF dual-hosted git repository. humbedooh pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-ponymail-foal.git
commit a1b4e77b70987a3cd4a1e10a1a2829a0a1824dbf Author: Daniel Gruno <[email protected]> AuthorDate: Mon Sep 7 21:57:35 2020 +0200 Implement sticky sessions across restarts --- server/plugins/session.py | 155 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 28 deletions(-) diff --git a/server/plugins/session.py b/server/plugins/session.py index 6c6f870..81d7295 100644 --- a/server/plugins/session.py +++ b/server/plugins/session.py @@ -28,7 +28,8 @@ import plugins.database import plugins.server import copy -PYPONY_MAX_SESSION_AGE = 86400 * 7 # Max 1 week between visits before voiding a session +FOAL_MAX_SESSION_AGE = 86400 * 7 # Max 1 week between visits before voiding a session +FOAL_SAVE_SESSION_INTERVAL = 3600 # Update sessions on disk max once per hour class SessionCredentials: @@ -40,52 +41,55 @@ class SessionCredentials: admin: bool oauth_data: dict - def __init__(self, doc: typing.Dict = None): + def __init__(self, doc): if doc: - self.uid = doc.get('uid', '') - self.name = doc.get('name', '') - self.email = doc.get('email', '') - self.provider = doc.get('provider', 'generic') - self.authoritative = doc.get('authoritative', False) - self.admin = doc.get('admin', False) - self.oauth_data = doc.get('oauth_data', {}) + self.uid = doc.get("uid", "") + self.name = doc.get("name", "") + self.email = doc.get("email", "") + self.oauth_provider = doc.get("oauth_provider", "generic") + self.authoritative = doc.get("authoritative", False) + self.admin = doc.get("admin", False) + self.oauth_data = doc.get("oauth_data", {}) else: self.uid = "" self.name = "" self.email = "" - self.provider = "generic" + self.oauth_provider = "generic" self.authoritative = False self.admin = False self.oauth_data = {} class SessionObject: - uid: str + cid: str + cookie: str created: int last_accessed: int credentials: SessionCredentials database: typing.Optional[plugins.database.Database] - def __init__(self, server: plugins.server.BaseServer, doc=None): + def __init__(self, server: plugins.server.BaseServer, **kwargs): self.database = None - if not doc: + if not kwargs: now = int(time.time()) self.created = now self.last_accessed = now - self.credentials = SessionCredentials() - self.uid = str(uuid.uuid4()) + self.credentials = None + self.cookie = str(uuid.uuid4()) + self.cid = None else: - self.created = doc["created"] - self.last_accessed = doc["last_accessed"] - self.credentials = SessionCredentials(doc["credentials"]) - self.uid = doc["uid"] + self.last_accessed = kwargs.get("last_accessed") + self.credentials = SessionCredentials(kwargs.get("credentials")) + self.cookie = kwargs.get("cookie") + self.cid = kwargs.get("cid") async def get_session( server: plugins.server.BaseServer, request: aiohttp.web.BaseRequest - ) -> SessionObject: +) -> SessionObject: session_id = None session = None + now = int(time.time()) if request.headers.get("cookie"): for cookie_header in request.headers.getall("cookie"): cookies: http.cookies.SimpleCookie = http.cookies.SimpleCookie( @@ -95,29 +99,124 @@ async def get_session( session_id = cookies["ponymail"].value break + # Do we have the session in local memory? if session_id in server.data.sessions: x_session = server.data.sessions[session_id] - now = int(time.time()) - if (now - x_session.last_accessed) > PYPONY_MAX_SESSION_AGE: + if (now - x_session.last_accessed) > FOAL_MAX_SESSION_AGE: del server.data.sessions[session_id] else: + + # Do we need to update the timestamp in ES? + if (now - x_session.last_accessed) > FOAL_SAVE_SESSION_INTERVAL: + x_session.last_accessed = now + await save_session(x_session) + # Make a copy so we don't have a race condition with the database pool object # In case the session is used twice within the same loop session = copy.copy(x_session) - if not session: - session = SessionObject(server) + session.database = await server.dbpool.get() + return session + + # If not in local memory, start a new session object + session = SessionObject(server) session.database = await server.dbpool.get() + + # If a cookie was supplied, look for a session object in ES + if session_id: + try: + session_doc = await session.database.get( + session.database.dbs.session, id=session_id + ) + last_update = session_doc["_source"]["updated"] + session.cookie = session_id + # Check that this cookie ain't too old. If it is, delete it and return bare-bones session object + if (now - last_update) > FOAL_MAX_SESSION_AGE: + session.database.delete( + index=session.database.dbs.session, id=session_id + ) + return session + + # Get CID and fecth the account data + cid = session_doc["_source"]["cid"] + if cid: + account_doc = await session.database.get(session.database.dbs.account, id=cid) + creds = account_doc["_source"]['credentials'] + internal = account_doc['_source']['internal'] + + # Set session data + session.cid = cid + session.last_accessed = last_update + creds["authoritative"] = ( + internal.get("oauth_provider") in server.config.oauth.authoritative_domains + ) + creds['oauth_provider'] = internal.get('oauth_provider', 'generic') + creds['oauth_data'] = internal.get('oauth_data', {}) + session.credentials = SessionCredentials(creds) + + # Save in memory storage + server.data.sessions[session_id] = session + + except plugins.database.DBError: + pass return session -async def set_session(server: plugins.server.BaseServer, **credentials): +async def set_session(server: plugins.server.BaseServer, cid, **credentials): """Create a new user session in the database""" session_id = str(uuid.uuid4()) cookie: http.cookies.SimpleCookie = http.cookies.SimpleCookie() cookie["ponymail"] = session_id - session = SessionObject(server) + session = SessionObject(server, last_accessed=time.time(), cookie=session_id, cid=cid) session.credentials = SessionCredentials(credentials) server.data.sessions[session_id] = session - return cookie['ponymail'].OutputString() - + # Grab temporary DB handle + session.database = await server.dbpool.get() + # Save session and account data + await save_session(session) + await save_credentials(session) + + # Put DB handle back + server.dbpool.put_nowait(session.database) + return cookie["ponymail"].OutputString() + + +async def save_session(session: SessionObject): + """Save a session object in the ES database""" + await session.database.index( + index=session.database.dbs.session, + id=session.cookie, + body={ + "cookie": session.cookie, + "cid": session.cid, + "updated": session.last_accessed, + }, + ) + + +async def remove_session(session: SessionObject): + """Remove a session object in the ES database""" + await session.database.delete( + index=session.database.dbs.session, + id=session.cookie + ) + + +async def save_credentials(session: SessionObject): + """Save a user account object in the ES database""" + await session.database.index( + index=session.database.dbs.account, + id=session.cid, + body={ + "cid": session.cid, + "credentials": { + "email": session.credentials.email, + "name": session.credentials.name, + "uid": session.credentials.uid, + }, + "internal": { + "oauth_provider": session.credentials.oauth_provider, + "oauth_data": session.credentials.oauth_data, + } + }, + )
