basis/components/web/utils/wrappers.py
apeters 3f3a7ba2d5 -
Signed-off-by: apeters <apeters@korves.net>
2025-05-27 07:25:27 +00:00

208 lines
6.5 KiB
Python

from .quart import abort, request, session, websocket, redirect, url_for
from .notifications import trigger_notification
from components.database import IN_MEMORY_DB
from components.logs import logger
from components.models import TypeAdapter, ValidationError
from components.objects import search as search_object
from components.users import UserSession, get, search as search_users, what_id
from config import defaults
from functools import wraps
class AuthException(Exception):
pass
def session_clear(preserved_keys: list = []) -> None:
if not preserved_keys:
preserved_keys = defaults.PRESERVE_SESSION_KEYS
restore_keys = set()
for k in preserved_keys:
session_key = session.get(k)
if session_key:
restore_keys.add(
(k, session_key),
)
session.clear()
for k in restore_keys:
session[k[0]] = k[1]
async def verify_session(acl: str) -> None:
if acl not in [*defaults.USER_ACLS, "any"]:
raise AuthException("Unknown ACL")
if not session.get("id"):
raise AuthException("Session ID missing")
if session["id"] not in IN_MEMORY_DB["SESSION_VALIDATED"]:
try:
user = await get(user_id=session["id"])
IN_MEMORY_DB["SESSION_VALIDATED"].update({session["id"]: user.acl})
session["acl"] = user.acl
except:
session_clear()
raise AuthException("User unknown")
if acl != "any":
if acl not in IN_MEMORY_DB["SESSION_VALIDATED"][session["id"]]:
raise AuthException("Access denied by ACL")
async def create_session_by_token(token):
if len(token.split(":")) != 2:
raise AuthException("Invalid access token format")
token_user, token_value = token.split(":")
try:
TypeAdapter(defaults.ACCESS_TOKEN_FORMAT).validate_python(token_value)
except ValidationError:
raise AuthException("Invalid token format")
try:
user_id = await what_id(login=token_user)
user = await get(user_id=user_id)
except:
session_clear()
raise AuthException("User unknown")
if token_value not in user.profile.access_tokens:
raise AuthException("Token unknown in user context")
user_session = UserSession(
login=user.login,
id=user.id,
acl=user.acl,
cred_id="",
lang=request.accept_languages.best_match(defaults.ACCEPT_LANGUAGES) or "en",
profile=user.profile,
)
for k, v in user_session.dict().items():
session[k] = v
def websocket_acl(acl_type):
def check_acl(fn):
@wraps(fn)
async def wrapper(*args, **kwargs):
try:
await verify_session(acl_type)
if not session["login"] in IN_MEMORY_DB["WS_CONNECTIONS"]:
IN_MEMORY_DB["WS_CONNECTIONS"][session["login"]] = dict()
if (
not websocket._get_current_object()
in IN_MEMORY_DB["WS_CONNECTIONS"][session["login"]]
):
IN_MEMORY_DB["WS_CONNECTIONS"][session["login"]][
websocket._get_current_object()
] = dict()
return await fn(*args, **kwargs)
except AuthException as e:
abort(401)
finally:
if "login" in session:
for ws in IN_MEMORY_DB["WS_CONNECTIONS"].get(session["login"], {}):
if ws == websocket._get_current_object():
del IN_MEMORY_DB["WS_CONNECTIONS"][session["login"]][ws]
break
return wrapper
return check_acl
def acl(acl_type):
def check_acl(fn):
@wraps(fn)
async def wrapper(*args, **kwargs):
try:
if "x-access-token" in request.headers:
await create_session_by_token(request.headers["x-access-token"])
await verify_session(acl_type)
return await fn(*args, **kwargs)
except AuthException as e:
client_addr = request.headers.get(
"X-Forwarded-For", request.remote_addr
)
logger.warning(
f'{client_addr} - {session.get("login")}[ID={session.get("id")}] tried to access {request.path}'
)
if "hx-request" in request.headers:
return trigger_notification(
level="error",
response_body="",
response_code=401,
title="Authentication Required",
message=str(e),
)
else:
if "x-access-token" in request.headers:
return (f"Authentication Required\n{str(e)}\n", 401)
return redirect(url_for("main.root"))
return wrapper
return check_acl
def formoptions(options):
async def form_options(option: list):
if option == "users":
return sorted(
[
{"name": user.login, "value": user.id, "groups": user.groups}
for user in await search_users(name="")
],
key=lambda x: x["name"],
)
else:
return sorted(
[
{"name": o.name, "value": o.id}
for o in await search_object(
object_type=option,
match_all={"assigned_users": [session["id"]]}
if not "system" in session["acl"]
else {},
)
],
key=lambda x: x["name"],
)
def inject_options(fn):
@wraps(fn)
async def wrapper(*args, **kwargs):
user_id = session["id"]
request.form_options = dict()
if not user_id in IN_MEMORY_DB["CACHE"]["FORMS"]:
IN_MEMORY_DB["CACHE"]["FORMS"][user_id] = dict()
for option in options:
if option not in IN_MEMORY_DB["CACHE"]["FORMS"][user_id]:
IN_MEMORY_DB["CACHE"]["FORMS"][user_id][
option
] = await form_options(option)
request.form_options[option] = IN_MEMORY_DB["CACHE"]["FORMS"][user_id][
option
]
return await fn(*args, **kwargs)
return wrapper
return inject_options