blob: e08cdc08a6e22d2d8572d890b0e9abb7bc685318 [file] [log] [blame]
#!/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 PyPony"""
import http.cookies
import time
import typing
import uuid
import aiohttp.web
import plugins.database
import plugins.server
import copy
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
DATABASE_NOT_CONNECTED = "Database not connected!"
OAUTH_PROVIDER_DEFAULT = "generic"
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", OAUTH_PROVIDER_DEFAULT)
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 = OAUTH_PROVIDER_DEFAULT
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]
remote: str
host: str
server: plugins.server.BaseServer
def __init__(self, server: plugins.server.BaseServer, **kwargs):
self.database = None
self.server = server
self.created = int(time.time())
self.host = "??"
self.remote = "??"
if kwargs:
self.last_accessed = kwargs.get("last_accessed", 0)
self.credentials = SessionCredentials(kwargs.get("credentials"))
self.cookie = kwargs.get("cookie", "___")
self.cid = kwargs.get("cid")
else:
self.last_accessed = self.created
self.credentials = None
self.cookie = str(uuid.uuid4())
self.cid = None
async def get_session(
server: plugins.server.BaseServer, 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 "ponymail" in cookies:
session_id = cookies["ponymail"].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) > FOAL_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()
session.host = request.headers.get("X-Forwarded-Host", request.host)
session.remote = request.remote
# Do we need to update the timestamp in ES?
if (now - session.last_accessed) > FOAL_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()
session.host = request.headers.get("X-Forwarded-Host", request.host or "??")
session.remote = request.remote or "??"
# 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.db_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.db_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.db_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", OAUTH_PROVIDER_DEFAULT)
creds["oauth_data"] = internal.get("oauth_data", {})
# We update admin boolean whenever we fetch session doc, as they may have changed in yaml but not in ES.
creds["admin"] = creds["authoritative"] and creds.get('email') in server.config.oauth.admins
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, cid: str, **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, last_accessed=int(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["ponymail"].OutputString()
async def save_session(session: SessionObject):
"""Save a session object in the ES database"""
assert session.database, DATABASE_NOT_CONNECTED
await session.database.index(
index=session.database.dbs.db_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.db_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.db_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,
"admin": session.credentials.admin,
},
},
)