[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 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): def login_user(username, password):
logger.info("login user {{ {} }}".format(username)) logger.info("login user {{ {} }}".format(username))
try: try:

View File

@ -54,6 +54,7 @@ class User(db.Model, ModelSerializeMixin):
firstname: str = db.Column(db.String(50), nullable=False) firstname: str = db.Column(db.String(50), nullable=False)
lastname: 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) 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) birthday: Optional[date] = db.Column(db.Date)
roles: [str] = [] roles: [str] = []

View File

@ -10,6 +10,20 @@ Args:
message: Message object to send 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") update_user_hook = HookCall("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:

View File

@ -2,16 +2,15 @@
import ssl import ssl
from typing import Optional 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_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 werkzeug.exceptions import BadRequest, InternalServerError
from flaschengeist import logger 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 from flaschengeist.models.user import User
import flaschengeist.controller.userController as userController import flaschengeist.controller.userController as userController
@ -19,10 +18,8 @@ import flaschengeist.controller.userController as userController
class AuthLDAP(AuthPlugin): class AuthLDAP(AuthPlugin):
def __init__(self, cfg): def __init__(self, cfg):
super().__init__() super().__init__()
config = {"port": 389, "use_ssl": False} config = {"port": 389, "use_ssl": False}
config.update(cfg) config.update(cfg)
app.config.update( app.config.update(
LDAP_SERVER=config["host"], LDAP_SERVER=config["host"],
LDAP_PORT=config["port"], LDAP_PORT=config["port"],
@ -38,6 +35,7 @@ class AuthLDAP(AuthPlugin):
self.ldap = LDAPConn(app) self.ldap = LDAPConn(app)
self.dn = config["base_dn"] self.dn = config["base_dn"]
self.default_gid = config["default_gid"] self.default_gid = config["default_gid"]
# TODO: might not be set if modify is called # TODO: might not be set if modify is called
if "admin_dn" in config: if "admin_dn" in config:
self.admin_dn = config["admin_dn"] self.admin_dn = config["admin_dn"]
@ -45,6 +43,14 @@ class AuthLDAP(AuthPlugin):
else: else:
self.admin_dn = None 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): def login(self, user, password):
if not user: if not user:
return False return False
@ -64,6 +70,7 @@ class AuthLDAP(AuthPlugin):
user.lastname = r["sn"][0] user.lastname = r["sn"][0]
if r["mail"]: if r["mail"]:
user.mail = r["mail"][0] user.mail = r["mail"][0]
user.avatar_url = f"/api/users/{user.userid}/avatar"
if "displayName" in r: if "displayName" in r:
user.display_name = r["displayName"][0] user.display_name = r["displayName"][0]
userController.set_roles(user, self._get_groups(user.userid), create=True) userController.set_roles(user, self._get_groups(user.userid), create=True)
@ -109,6 +116,75 @@ class AuthLDAP(AuthPlugin):
except (LDAPPasswordIsMandatoryError, LDAPBindError): except (LDAPPasswordIsMandatoryError, LDAPBindError):
raise BadRequest 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): def _get_groups(self, uid):
groups = [] groups = []
self.ldap.connection.search( self.ldap.connection.search(
@ -159,49 +235,3 @@ class AuthLDAP(AuthPlugin):
except (LDAPPasswordIsMandatoryError, LDAPBindError): except (LDAPPasswordIsMandatoryError, LDAPBindError):
raise BadRequest 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 http.client import NO_CONTENT, CREATED
from flaschengeist.config import config from flaschengeist.config import config
from flask import Blueprint, request, jsonify, make_response from flask import Blueprint, request, jsonify, make_response, Response
from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed, NotFound
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.models.user import User from flaschengeist.models.user import User
from flaschengeist.plugins import Plugin from flaschengeist.plugins import Plugin
from flaschengeist.decorator import login_required, extract_session from flaschengeist.decorator import login_required, extract_session
from flaschengeist.controller import userController from flaschengeist.controller import userController
from flaschengeist.utils.HTTP import created
from flaschengeist.utils.datetime import from_iso_format from flaschengeist.utils.datetime import from_iso_format
users_bp = Blueprint("users", __name__) users_bp = Blueprint("users", __name__)
@ -93,6 +94,36 @@ def get_user(userid, current_session):
return jsonify(serial) 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"]) @users_bp.route("/users/<userid>", methods=["DELETE"])
@login_required(permission=_permission_delete) @login_required(permission=_permission_delete)
def delete_user(userid, current_session): def delete_user(userid, current_session):