From d0674e8876c21a53ab97e7aebfaf622097eae54a Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 2 Dec 2021 21:27:59 +0100 Subject: [PATCH] fix(users): Fix deleting users Remove all internal references, e.g. sessions, attributes, password reset requests. Add hook for plugins. If not deletable remove at least all personal data --- flaschengeist/controller/userController.py | 26 +++++++++++++++++++- flaschengeist/models/user.py | 3 +-- flaschengeist/plugins/__init__.py | 5 ++++ flaschengeist/plugins/auth_plain/__init__.py | 5 ++-- flaschengeist/plugins/users/__init__.py | 19 ++++++++------ 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/flaschengeist/controller/userController.py b/flaschengeist/controller/userController.py index 944565e..2c81e64 100644 --- a/flaschengeist/controller/userController.py +++ b/flaschengeist/controller/userController.py @@ -1,5 +1,6 @@ import secrets from io import BytesIO +from sqlalchemy import exc from flask import current_app from datetime import datetime, timedelta, timezone from flask.helpers import send_file @@ -168,11 +169,34 @@ def find_user(uid_mail): return user -def delete(user): +@Hook +def delete_user(user: User): """Delete given user""" + # First let the backend delete the user, as this might fail current_app.config["FG_AUTH_BACKEND"].delete_user(user) + # Clear all easy relationships + user.avatar_ = None + user._attributes.clear() + user.roles_.clear() + user.sessions_.clear() + user.reset_requests_.clear() + db.session.commit() + try: + # Delete the user db.session.delete(user) db.session.commit() + except exc.IntegrityError: + logger.error("Delete of user failed, there might be ForeignKey contraits from disabled plugins", exec_info=True) + # Remove at least all personal data + user.userid = f"__deleted_user__{user.id_}" + user.display_name = "DELETED USER" + user.firstname = "" + user.lastname = "" + user.birthday = None + user.mail = None + db.session.commit() + + def register(data): diff --git a/flaschengeist/models/user.py b/flaschengeist/models/user.py index 67c4142..31da202 100644 --- a/flaschengeist/models/user.py +++ b/flaschengeist/models/user.py @@ -5,10 +5,9 @@ from typing import Optional from datetime import date, datetime from sqlalchemy.orm.collections import attribute_mapped_collection -from flaschengeist.models.image import Image - from ..database import db from . import ModelSerializeMixin, UtcDateTime, Serial +from .image import Image association_table = db.Table( diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index 089c5d5..4f253ab 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -27,6 +27,11 @@ before_update_user = HookBefore("update_user") Args: user: User object """ +before_delete_user = HookBefore("delete_user") +"""Hook decorator,this is called before an user gets deleted. +Args: + user: User object +""" class Plugin: diff --git a/flaschengeist/plugins/auth_plain/__init__.py b/flaschengeist/plugins/auth_plain/__init__.py index 4db87d8..7a95679 100644 --- a/flaschengeist/plugins/auth_plain/__init__.py +++ b/flaschengeist/plugins/auth_plain/__init__.py @@ -6,8 +6,7 @@ Flaschengeist database (as User attribute) import os import hashlib import binascii -from werkzeug.exceptions import BadRequest, NotFound - +from werkzeug.exceptions import BadRequest from flaschengeist.plugins import AuthPlugin from flaschengeist.models.user import User, Role, Permission from flaschengeist.database import db @@ -18,6 +17,8 @@ class AuthPlain(AuthPlugin): def post_install(self): if User.query.first() is None: logger.info("Installing admin user") + role = Role.query.filter(Role.name == "Superuser").first() + if role is None: role = Role(name="Superuser", permissions=Permission.query.all()) admin = User( userid="admin", diff --git a/flaschengeist/plugins/users/__init__.py b/flaschengeist/plugins/users/__init__.py index 75f2f56..b368753 100644 --- a/flaschengeist/plugins/users/__init__.py +++ b/flaschengeist/plugins/users/__init__.py @@ -2,18 +2,17 @@ Provides routes used to manage users """ -from io import BytesIO from http.client import NO_CONTENT, CREATED -from flask import Blueprint, request, jsonify, make_response, Response, send_file +from flask import Blueprint, request, jsonify, make_response from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed, NotFound from . import permissions from flaschengeist import logger from flaschengeist.config import config from flaschengeist.plugins import Plugin -from flaschengeist.models.user import User, _Avatar +from flaschengeist.models.user import User from flaschengeist.utils.decorators import login_required, extract_session, headers -from flaschengeist.controller import userController, imageController as image_controller +from flaschengeist.controller import userController from flaschengeist.utils.HTTP import created, no_content from flaschengeist.utils.datetime import from_iso_format @@ -23,6 +22,10 @@ class UsersPlugin(Plugin): blueprint = Blueprint(name, __name__) permissions = permissions.permissions + def install(self): + userController.install() + return super().install() + @UsersPlugin.blueprint.route("/users", methods=["POST"]) def register(): @@ -144,7 +147,7 @@ def delete_avatar(userid, current_session): if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.EDIT): raise Forbidden userController.delete_avatar(user) - return "", NO_CONTENT + return no_content() @UsersPlugin.blueprint.route("/users/", methods=["DELETE"]) @@ -163,8 +166,8 @@ def delete_user(userid, current_session): """ logger.debug("Delete user {{ {} }}".format(userid)) user = userController.get_user(userid) - userController.delete(user) - return "", NO_CONTENT + userController.delete_user(user) + return no_content() @UsersPlugin.blueprint.route("/users/", methods=["PUT"]) @@ -217,7 +220,7 @@ def edit_user(userid, current_session): userController.modify_user(user, password, new_password) userController.update_user(user) - return "", NO_CONTENT + return no_content() @UsersPlugin.blueprint.route("/notifications", methods=["GET"])