[System][Plugin] Improved Hooks, roles and auth_ldap improvements

* Hooks now allow multiple hooked functions
* Hooks can now be called before and after a function call
* Fixed issue in datetime util when string is None or empty
* Roles: Return new created role as json
* auth_ldap: Use new Hooks
* auth_ldap: Fixed an issue where ldap response is not checked (when role gets renamed)
This commit is contained in:
Ferdinand Thiessen 2020-11-18 00:39:25 +01:00
parent 58e121473b
commit 2d6b86e2eb
7 changed files with 72 additions and 57 deletions

View File

@ -26,21 +26,21 @@ def get_permissions():
@Hook @Hook
def role_updated(role, old_name): def update_role(role, new_name):
"""Hook used when roles are updated""" if new_name is None:
pass try:
logger.debug(f"Hallo, dies ist die {role.serialize()}")
db.session.delete(role)
def rename(role, new_name): logger.debug(f"Hallo, dies ist die {role.serialize()}")
if role.name == new_name: db.session.commit()
return except IntegrityError:
if db.session.query(db.exists().where(Role.name == new_name)).scalar(): logger.debug("IntegrityError: Role might still be in use", exc_info=True)
raise BadRequest("Name already used") raise BadRequest("Role still in use")
elif role.name != new_name:
old_name = role.name if db.session.query(db.exists().where(Role.name == new_name)).scalar():
role.name = new_name raise BadRequest("Name already used")
role_updated(role, old_name) role.name = new_name
db.session.commit() db.session.commit()
def set_permissions(role, permissions): def set_permissions(role, permissions):
@ -63,18 +63,15 @@ def create_permissions(permissions):
def create_role(name: str, permissions=[]): def create_role(name: str, permissions=[]):
logger.debug(f"Create new role with name: {name}")
role = Role(name=name) role = Role(name=name)
db.session.add(role) db.session.add(role)
set_permissions(role, permissions) set_permissions(role, permissions)
db.session.commit()
logger.debug(f"Created role: {role.serialize()}")
return role return role
def delete(role): def delete(role):
role.permissions.clear() role.permissions.clear()
try: update_role(role, None)
db.session.delete(role)
db.session.commit()
except IntegrityError:
logger.debug("IntegrityError: Role might still be in use", exc_info=True)
raise BadRequest("Role still in use")
role_updated(None, role.name)

View File

@ -101,5 +101,5 @@ def save_avatar(user, avatar):
user.avatar_url = "" user.avatar_url = ""
r = current_app.config["FG_AUTH_BACKEND"].set_avatar(user, avatar) r = current_app.config["FG_AUTH_BACKEND"].set_avatar(user, avatar)
if not user.avatar_url: if not user.avatar_url:
user.avatar_url = url_for('users.get_avatar', userid=user.userid) user.avatar_url = url_for("users.get_avatar", userid=user.userid)
db.session.commit() db.session.commit()

View File

@ -2,22 +2,21 @@ import pkg_resources
from werkzeug.exceptions import MethodNotAllowed, NotFound from werkzeug.exceptions import MethodNotAllowed, NotFound
from flaschengeist.models.user import _Avatar from flaschengeist.models.user import _Avatar
from flaschengeist.utils.hook import HookCall from flaschengeist.utils.hook import HookBefore, HookAfter
send_message_hook = HookCall("send_message") before_role_updated = HookBefore("update_role")
"""Hook decorator for sending messages, register to send the message
Args:
message: Message object to send
"""
role_updated = HookCall("role_updated")
"""Hook decorator for when roles are modified """Hook decorator for when roles are modified
Args: Args:
role: Role object containing the modified role (None if deleted) role: Role object to modify
old_name: Old name if the name was changed new_name: New name if the name was changed (None if delete)
""" """
after_role_updated = HookAfter("update_role")
update_user_hook = HookCall("update_user") """Hook decorator for when roles are modified
Args:
role: Role object containing the modified role
new_name: New name if the name was changed (None if deleted)
"""
before_update_user = HookBefore("update_user")
"""Hook decorator, when ever an user update is done, this is called before. """Hook decorator, when ever an user update is done, this is called before.
Args: Args:
user: User object user: User object

View File

@ -10,7 +10,7 @@ from ldap3 import SUBTREE, MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE, HASHED_SAL
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.plugins import AuthPlugin, role_updated from flaschengeist.plugins import AuthPlugin, after_role_updated
from flaschengeist.models.user import User, Role, _Avatar from flaschengeist.models.user import User, Role, _Avatar
import flaschengeist.controller.userController as userController import flaschengeist.controller.userController as userController
@ -43,9 +43,9 @@ class AuthLDAP(AuthPlugin):
else: else:
self.admin_dn = None self.admin_dn = None
@role_updated @after_role_updated
def _role_updated(role, old_name): def _role_updated(role, new_name):
self.__modify_role(role, old_name) self.__modify_role(role, new_name)
def login(self, user, password): def login(self, user, password):
if not user: if not user:
@ -183,19 +183,19 @@ class AuthLDAP(AuthPlugin):
def __modify_role( def __modify_role(
self, self,
role: Optional[Role], role: Role,
old_name: str, new_name: Optional[str],
): ):
if self.admin_dn is None: if self.admin_dn is None:
logger.error("admin_dn missing in ldap config!") logger.error("admin_dn missing in ldap config!")
raise InternalServerError raise InternalServerError
try: try:
ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret)
ldap_conn.search(f"ou=group,{self.dn}", f"(cn={old_name})", SUBTREE, attributes=["cn"]) ldap_conn.search(f"ou=group,{self.dn}", f"(cn={role.name})", SUBTREE, attributes=["cn"])
if len(ldap_conn.response) >= 0: if len(ldap_conn.response) > 0:
dn = ldap_conn.response[0]["dn"] dn = ldap_conn.response[0]["dn"]
if role: if new_name:
ldap_conn.modify_dn(dn, f"cn={role.name}") ldap_conn.modify_dn(dn, f"cn={new_name}")
else: else:
ldap_conn.delete(dn) ldap_conn.delete(dn)

View File

@ -10,6 +10,7 @@ from http.client import CREATED, NO_CONTENT
from flaschengeist.plugins import Plugin from flaschengeist.plugins import Plugin
from flaschengeist.decorator import login_required from flaschengeist.decorator import login_required
from flaschengeist.controller import roleController from flaschengeist.controller import roleController
from flaschengeist.utils.HTTP import created
roles_bp = Blueprint("roles", __name__) roles_bp = Blueprint("roles", __name__)
_permission_edit = "roles_edit" _permission_edit = "roles_edit"
@ -51,15 +52,14 @@ def create_role(current_session):
current_session: Session sent with Authorization Header current_session: Session sent with Authorization Header
Returns: Returns:
HTTP-201 or HTTP error HTTP-201 and json encoded created Role or HTTP error
""" """
data = request.get_json() data = request.get_json()
if not data or "name" not in data: if not data or "name" not in data:
raise BadRequest raise BadRequest
if "permissions" in data: if "permissions" in data:
permissions = data["permissions"] permissions = data["permissions"]
roleController.create_role(data["name"], permissions) return created(roleController.create_role(data["name"], permissions))
return "", CREATED
@roles_bp.route("/roles/permissions", methods=["GET"]) @roles_bp.route("/roles/permissions", methods=["GET"])
@ -116,10 +116,10 @@ def edit_role(role_id, current_session):
role = roleController.get(role_id) role = roleController.get(role_id)
data = request.get_json() data = request.get_json()
if "name" in data:
roleController.rename(role, data["name"])
if "permissions" in data: if "permissions" in data:
roleController.set_permissions(role, data["permissions"]) roleController.set_permissions(role, data["permissions"])
if "name" in data:
roleController.update_role(role, data["name"])
return "", NO_CONTENT return "", NO_CONTENT

View File

@ -6,6 +6,8 @@ MonkeyPatch.patch_fromisoformat()
def from_iso_format(date_str): def from_iso_format(date_str):
"""Z-suffix aware version of `datetime.datetime.fromisoformat`""" """Z-suffix aware version of `datetime.datetime.fromisoformat`"""
if not date_str:
return None
time = datetime.datetime.fromisoformat(date_str.replace("Z", "+00:00")) time = datetime.datetime.fromisoformat(date_str.replace("Z", "+00:00"))
if time.tzinfo: if time.tzinfo:
return time.astimezone(datetime.timezone.utc) return time.astimezone(datetime.timezone.utc)

View File

@ -1,4 +1,4 @@
_hook_dict = {} _hook_dict = ({}, {})
class Hook(object): class Hook(object):
@ -10,17 +10,34 @@ class Hook(object):
self.function = function self.function = function
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
if self.function.__name__ in _hook_dict: # Hooks before
_hook_dict[self.function.__name__](*args, **kwargs) for function in _hook_dict[0].get(self.function.__name__, []):
self.function(*args, **kwargs) function(*args, **kwargs)
# Main function
ret = self.function(*args, **kwargs)
# Hooks after
for function in _hook_dict[1].get(self.function.__name__, []):
function(*args, **kwargs)
return ret
class HookCall(object): class HookBefore(object):
"""Decorator for functions to be called if a Hook is called""" """Decorator for functions to be called before a Hook-Function is called"""
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
def __call__(self, function): def __call__(self, function):
_hook_dict[self.name] = function _hook_dict[0].setdefault(self.name, []).append(function)
return function
class HookAfter(object):
"""Decorator for functions to be called after a Hook-Function is called"""
def __init__(self, name):
self.name = name
def __call__(self, function):
_hook_dict[1].setdefault(self.name, []).append(function)
return function return function