[Plugin]users, auth_ldap: Implemented avatar
This commit is contained in:
parent
9409533f7c
commit
a270857c41
|
@ -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:
|
||||||
|
|
|
@ -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] = []
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue