From a270857c41177747ae0349b152306a69035cbc70 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 16 Nov 2020 02:30:24 +0100 Subject: [PATCH] [Plugin]users, auth_ldap: Implemented avatar --- flaschengeist/controller/userController.py | 15 +++ flaschengeist/models/user.py | 1 + flaschengeist/plugins/__init__.py | 14 ++ flaschengeist/plugins/auth_ldap/__init__.py | 138 ++++++++++++-------- flaschengeist/plugins/users/__init__.py | 35 ++++- 5 files changed, 147 insertions(+), 56 deletions(-) diff --git a/flaschengeist/controller/userController.py b/flaschengeist/controller/userController.py index 776dbce..5db035b 100644 --- a/flaschengeist/controller/userController.py +++ b/flaschengeist/controller/userController.py @@ -8,6 +8,21 @@ from flaschengeist.database import db from flaschengeist import logger +class Avatar: + mimetype = "" + binary = bytearray() + + +@Hook +def load_avatar(avatar: Avatar, user: User): + pass + + +@Hook +def save_avatar(avatar: Avatar, user: User): + pass + + def login_user(username, password): logger.info("login user {{ {} }}".format(username)) try: diff --git a/flaschengeist/models/user.py b/flaschengeist/models/user.py index eec15d6..f938046 100644 --- a/flaschengeist/models/user.py +++ b/flaschengeist/models/user.py @@ -54,6 +54,7 @@ class User(db.Model, ModelSerializeMixin): firstname: str = db.Column(db.String(50), nullable=False) lastname: str = db.Column(db.String(50), nullable=False) mail: str = db.Column(db.String(60), nullable=False) + avatar_url: Optional[str] = db.Column(db.String(60)) birthday: Optional[date] = db.Column(db.Date) roles: [str] = [] diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index 2082116..ed7a82b 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -10,6 +10,20 @@ Args: message: Message object to send """ +load_avatar_hook = HookCall("load_avatar") +"""Hook decorator for loading Avatar data +Args: + avatar: Avatar object + user: User object to load from +""" + +save_avatar_hook = HookCall("save_avatar") +"""Hook decorator for saving Avatar data +Args: + avatar: Avatar object + user: User object to save +""" + update_user_hook = HookCall("update_user") """Hook decorator, when ever an user update is done, this is called before. Args: diff --git a/flaschengeist/plugins/auth_ldap/__init__.py b/flaschengeist/plugins/auth_ldap/__init__.py index 482630b..f421c19 100644 --- a/flaschengeist/plugins/auth_ldap/__init__.py +++ b/flaschengeist/plugins/auth_ldap/__init__.py @@ -2,16 +2,15 @@ import ssl from typing import Optional - -from ldap3.utils.hashed import hashed -from ldap3 import SUBTREE, MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE, HASHED_SALTED_MD5 -from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError -from flask import current_app as app from flask_ldapconn import LDAPConn +from flask import current_app as app +from ldap3.utils.hashed import hashed +from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError +from ldap3 import SUBTREE, MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE, HASHED_SALTED_MD5 from werkzeug.exceptions import BadRequest, InternalServerError from flaschengeist import logger -from flaschengeist.plugins import AuthPlugin +from flaschengeist.plugins import AuthPlugin, load_avatar_hook, save_avatar_hook from flaschengeist.models.user import User import flaschengeist.controller.userController as userController @@ -19,10 +18,8 @@ import flaschengeist.controller.userController as userController class AuthLDAP(AuthPlugin): def __init__(self, cfg): super().__init__() - config = {"port": 389, "use_ssl": False} config.update(cfg) - app.config.update( LDAP_SERVER=config["host"], LDAP_PORT=config["port"], @@ -38,6 +35,7 @@ class AuthLDAP(AuthPlugin): self.ldap = LDAPConn(app) self.dn = config["base_dn"] self.default_gid = config["default_gid"] + # TODO: might not be set if modify is called if "admin_dn" in config: self.admin_dn = config["admin_dn"] @@ -45,6 +43,14 @@ class AuthLDAP(AuthPlugin): else: self.admin_dn = None + @load_avatar_hook + def load_avatar(avatar, user): + self.__load_avatar(avatar, user) + + @save_avatar_hook + def load_avatar(avatar, user): + self.__save_avatar(avatar, user) + def login(self, user, password): if not user: return False @@ -64,6 +70,7 @@ class AuthLDAP(AuthPlugin): user.lastname = r["sn"][0] if r["mail"]: user.mail = r["mail"][0] + user.avatar_url = f"/api/users/{user.userid}/avatar" if "displayName" in r: user.display_name = r["displayName"][0] userController.set_roles(user, self._get_groups(user.userid), create=True) @@ -109,6 +116,75 @@ class AuthLDAP(AuthPlugin): except (LDAPPasswordIsMandatoryError, LDAPBindError): raise BadRequest + def modify_role(self, old_name: str, new_name: Optional[str]): + if self.admin_dn is None: + logger.error("admin_dn missing in ldap config!") + raise InternalServerError + try: + 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"]) + if len(ldap_conn.response) >= 0: + dn = ldap_conn.response[0]["dn"] + if new_name: + ldap_conn.modify_dn(dn, f"cn={new_name}") + else: + ldap_conn.delete(dn) + + except (LDAPPasswordIsMandatoryError, LDAPBindError): + raise BadRequest + + def modify_user(self, user: User, password=None, new_password=None): + try: + dn = user.get_attribute("DN") + if password: + ldap_conn = self.ldap.connect(dn, password) + else: + if self.admin_dn is None: + logger.error("admin_dn missing in ldap config!") + raise InternalServerError + ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) + modifier = {} + for name, ldap_name in [ + ("firstname", "givenName"), + ("lastname", "sn"), + ("mail", "mail"), + ("display_name", "displayName"), + ]: + if hasattr(user, name): + modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])] + if new_password: + # TODO: Use secure hash! + salted_password = hashed(HASHED_SALTED_MD5, new_password) + modifier["userPassword"] = [(MODIFY_REPLACE, [salted_password])] + ldap_conn.modify(dn, modifier) + self._set_roles(user) + except (LDAPPasswordIsMandatoryError, LDAPBindError): + raise BadRequest + + def __load_avatar(self, avatar, user): + self.ldap.connection.search( + "ou=user,{}".format(self.dn), + "(uid={})".format(user.userid), + SUBTREE, + attributes=["uid", "jpegPhoto"], + ) + r = self.ldap.connection.response[0]["attributes"] + if r["uid"][0] == user.userid: + avatar.mimetype = "image/jpeg" + avatar.binary.clear() + avatar.binary.extend(r["jpegPhoto"][0]) + + def __save_avatar(self, avatar, user): + if avatar.mimetype != "image/jpeg": + raise BadRequest("Unsupported image Format") + # Maybe use Pillow to convert here + if self.admin_dn is None: + logger.error("admin_dn missing in ldap config!") + raise InternalServerError + dn = user.get_attribute("DN") + ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) + ldap_conn.modify(dn, {"jpegPhoto": [(MODIFY_REPLACE, [avatar.binary])]}) + def _get_groups(self, uid): groups = [] self.ldap.connection.search( @@ -159,49 +235,3 @@ class AuthLDAP(AuthPlugin): except (LDAPPasswordIsMandatoryError, LDAPBindError): raise BadRequest - - def modify_role(self, old_name: str, new_name: Optional[str]): - if self.admin_dn is None: - logger.error("admin_dn missing in ldap config!") - raise InternalServerError - try: - 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"]) - if len(ldap_conn.response) >= 0: - dn = ldap_conn.response[0]["dn"] - if new_name: - ldap_conn.modify_dn(dn, f"cn={new_name}") - else: - ldap_conn.delete(dn) - - except (LDAPPasswordIsMandatoryError, LDAPBindError): - raise BadRequest - - def modify_user(self, user: User, password=None, new_password=None): - if self.admin_dn is None: - logger.error("admin_dn missing in ldap config!") - raise InternalServerError - - try: - dn = user.get_attribute("DN") - if password: - ldap_conn = self.ldap.connect(dn, password) - else: - ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) - modifier = {} - for name, ldap_name in [ - ("firstname", "givenName"), - ("lastname", "sn"), - ("mail", "mail"), - ("display_name", "displayName"), - ]: - if hasattr(user, name): - modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])] - if new_password: - # TODO: Use secure hash! - salted_password = hashed(HASHED_SALTED_MD5, new_password) - modifier["userPassword"] = [(MODIFY_REPLACE, [salted_password])] - ldap_conn.modify(dn, modifier) - self._set_roles(user) - except (LDAPPasswordIsMandatoryError, LDAPBindError): - raise BadRequest diff --git a/flaschengeist/plugins/users/__init__.py b/flaschengeist/plugins/users/__init__.py index 71595af..3e39532 100644 --- a/flaschengeist/plugins/users/__init__.py +++ b/flaschengeist/plugins/users/__init__.py @@ -5,14 +5,15 @@ Provides routes used to manage users from http.client import NO_CONTENT, CREATED from flaschengeist.config import config -from flask import Blueprint, request, jsonify, make_response -from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed +from flask import Blueprint, request, jsonify, make_response, Response +from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed, NotFound from flaschengeist import logger from flaschengeist.models.user import User from flaschengeist.plugins import Plugin from flaschengeist.decorator import login_required, extract_session from flaschengeist.controller import userController +from flaschengeist.utils.HTTP import created from flaschengeist.utils.datetime import from_iso_format users_bp = Blueprint("users", __name__) @@ -93,6 +94,36 @@ def get_user(userid, current_session): return jsonify(serial) +@users_bp.route("/users//avatar", methods=["GET"]) +def get_avatar(userid): + user = userController.get_user(userid) + avatar = userController.Avatar() + userController.load_avatar(avatar, user) + if len(avatar.binary) > 0: + response = Response(avatar.binary, mimetype=avatar.mimetype) + response.add_etag() + return response.make_conditional(request) + raise NotFound + + +@users_bp.route("/users//avatar", methods=["POST"]) +@login_required() +def set_avatar(userid, current_session): + user = userController.get_user(userid) + if userid != current_session._user.userid and not current_session._user.has_permission(_permission_edit): + raise Forbidden + + file = request.files.get("file") + if file: + avatar = userController.Avatar() + avatar.mimetype = file.content_type + avatar.binary = bytearray(file.stream.read()) + userController.save_avatar(avatar, user) + return created() + else: + raise BadRequest + + @users_bp.route("/users/", methods=["DELETE"]) @login_required(permission=_permission_delete) def delete_user(userid, current_session):