Add session plugin
This is copied from Foal, and does not work yet. It is there so the
program will run.
diff --git a/server/plugins/session.py b/server/plugins/session.py
new file mode 100644
index 0000000..7fbfd44
--- /dev/null
+++ b/server/plugins/session.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This is the user session handler for Boxer"""
+
+import http.cookies
+import time
+import typing
+import uuid
+
+import aiohttp.web
+
+import plugins.database
+import plugins.basetypes
+import copy
+import typing
+
+MAX_SESSION_AGE = 86400 * 7 # Max 1 week between visits before voiding a session
+SAVE_SESSION_INTERVAL = 3600 # Update sessions on disk max once per hour
+
+
+class SessionCredentials:
+ uid: str
+ name: str
+ email: str
+ provider: str
+ authoritative: bool
+ admin: bool
+ oauth_data: dict
+
+ def __init__(self, doc):
+ if doc:
+ 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.oauth_provider = "generic"
+ self.authoritative = False
+ self.admin = False
+ self.oauth_data = {}
+
+
+class SessionObject:
+ cid: typing.Optional[str]
+ cookie: str
+ created: int
+ last_accessed: int
+ credentials: typing.Optional[SessionCredentials]
+ database: typing.Optional[plugins.database.Database]
+ server: plugins.basetypes.Server
+
+ def __init__(self, server: plugins.basetypes.Server, **kwargs):
+ self.database = None
+ self.server = server
+ if not kwargs:
+ now = int(time.time())
+ self.created = now
+ self.last_accessed = now
+ self.credentials = None
+ self.cookie = str(uuid.uuid4())
+ self.cid = None
+ else:
+ self.last_accessed = kwargs.get("last_accessed", 0)
+ self.credentials = SessionCredentials(kwargs.get("credentials"))
+ self.cookie = kwargs.get("cookie", "___")
+ self.cid = kwargs.get("cid")
+
+
+async def get_session(
+ server: plugins.basetypes.Server, request: aiohttp.web.BaseRequest
+) -> 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(
+ cookie_header
+ )
+ if "boxer" in cookies:
+ session_id = cookies["boxer"].value
+ if not all(c in "abcdefg1234567890-" for c in session_id):
+ session_id = None
+ break
+
+ # Do we have the session in local memory?
+ if session_id and session_id in server.data.sessions:
+ x_session = server.data.sessions[session_id]
+ if (now - x_session.last_accessed) > MAX_SESSION_AGE:
+ del server.data.sessions[session_id]
+ else:
+
+ # 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)
+ session.database = await server.dbpool.get()
+
+ # Do we need to update the timestamp in ES?
+ if (now - session.last_accessed) > SAVE_SESSION_INTERVAL:
+ session.last_accessed = now
+ await save_session(session)
+
+ 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 and session.database:
+ 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) > 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.basetypes.Server, cid, **credentials):
+ """Create a new user session in the database"""
+ session_id = str(uuid.uuid4())
+ cookie: http.cookies.SimpleCookie = http.cookies.SimpleCookie()
+ cookie["boxer"] = session_id
+ session = SessionObject(
+ server, last_accessed=time.time(), cookie=session_id, cid=cid
+ )
+ session.credentials = SessionCredentials(credentials)
+ server.data.sessions[session_id] = session
+
+ # Grab temporary DB handle since session objects at init do not have this
+ # We just need this to be able to save the session in ES.
+ session.database = await server.dbpool.get()
+
+ # Save session and account data
+ await save_session(session)
+ await save_credentials(session)
+
+ # Put DB handle back into the pool
+ server.dbpool.put_nowait(session.database)
+ return cookie["boxer"].OutputString()
+
+
+async def save_session(session: SessionObject):
+ """Save a session object in the database"""
+ assert session.database, "Database not connected!"
+ 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"""
+ assert session.database, "Database not connected!"
+ 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"""
+ assert session.database, "Database not connected!"
+ assert session.credentials, "Session object without credentials, cannot save!"
+ 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,
+ },
+ },
+ )