[Plugin]users, auth_ldap: Implemented avatar

This commit is contained in:
Ferdinand Thiessen 2020-11-16 02:30:24 +01:00
parent 9409533f7c
commit a270857c41
5 changed files with 147 additions and 56 deletions

View File

@ -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:

View File

@ -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] = []

View File

@ -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:

View File

@ -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

View File

@ -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/<userid>/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/<userid>/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/<userid>", methods=["DELETE"])
@login_required(permission=_permission_delete)
def delete_user(userid, current_session):