[Plugin] Use plugin function instead of HookCall
This commit is contained in:
parent
39a259a693
commit
28865649b4
|
@ -1,10 +1,10 @@
|
||||||
from flask import current_app
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from werkzeug.exceptions import BadRequest, NotFound
|
from werkzeug.exceptions import BadRequest, NotFound
|
||||||
|
|
||||||
from flaschengeist.models.user import Role, Permission
|
from flaschengeist.models.user import Role, Permission
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
|
from flaschengeist.utils.hook import Hook
|
||||||
|
|
||||||
|
|
||||||
def get_all():
|
def get_all():
|
||||||
|
@ -25,14 +25,21 @@ def get_permissions():
|
||||||
return Permission.query.all()
|
return Permission.query.all()
|
||||||
|
|
||||||
|
|
||||||
|
@Hook
|
||||||
|
def role_updated(role, old_name):
|
||||||
|
"""Hook used when roles are updated"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def rename(role, new_name):
|
def rename(role, new_name):
|
||||||
if role.name == new_name:
|
if role.name == new_name:
|
||||||
return
|
return
|
||||||
if db.session.query(db.exists().where(Role.name == new_name)).scalar():
|
if db.session.query(db.exists().where(Role.name == new_name)).scalar():
|
||||||
raise BadRequest("Name already used")
|
raise BadRequest("Name already used")
|
||||||
|
|
||||||
current_app.config["FG_AUTH_BACKEND"].modify_role(role.name, new_name)
|
old_name = role.name
|
||||||
role.name = new_name
|
role.name = new_name
|
||||||
|
role_updated(role, old_name)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,4 +77,4 @@ def delete(role):
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
logger.debug("IntegrityError: Role might still be in use", exc_info=True)
|
logger.debug("IntegrityError: Role might still be in use", exc_info=True)
|
||||||
raise BadRequest("Role still in use")
|
raise BadRequest("Role still in use")
|
||||||
current_app.config["FG_AUTH_BACKEND"].modify_role(role.name, None)
|
role_updated(None, role.name)
|
||||||
|
|
|
@ -8,21 +8,6 @@ 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:
|
||||||
|
@ -106,3 +91,11 @@ def register(data):
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def load_avatar(user: User):
|
||||||
|
return current_app.config["FG_AUTH_BACKEND"].get_avatar(user)
|
||||||
|
|
||||||
|
|
||||||
|
def save_avatar(user, avatar):
|
||||||
|
return current_app.config["FG_AUTH_BACKEND"].set_avatar(user, avatar)
|
||||||
|
|
|
@ -96,3 +96,10 @@ class _UserAttribute(db.Model, ModelSerializeMixin):
|
||||||
user: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
user: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
name: str = db.Column(db.String(30))
|
name: str = db.Column(db.String(30))
|
||||||
value: any = db.Column(db.PickleType(protocol=4))
|
value: any = db.Column(db.PickleType(protocol=4))
|
||||||
|
|
||||||
|
|
||||||
|
class _Avatar:
|
||||||
|
"""Wrapper class for avatar binaries"""
|
||||||
|
|
||||||
|
mimetype = ""
|
||||||
|
binary = bytearray()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from typing import Optional
|
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
||||||
from werkzeug.exceptions import MethodNotAllowed
|
|
||||||
|
|
||||||
|
from flaschengeist.models.user import _Avatar
|
||||||
from flaschengeist.utils.hook import HookCall
|
from flaschengeist.utils.hook import HookCall
|
||||||
|
|
||||||
send_message_hook = HookCall("send_message")
|
send_message_hook = HookCall("send_message")
|
||||||
|
@ -10,18 +10,11 @@ Args:
|
||||||
message: Message object to send
|
message: Message object to send
|
||||||
"""
|
"""
|
||||||
|
|
||||||
load_avatar_hook = HookCall("load_avatar")
|
role_updated = HookCall("role_updated")
|
||||||
"""Hook decorator for loading Avatar data
|
"""Hook decorator for when roles are modified
|
||||||
Args:
|
Args:
|
||||||
avatar: Avatar object
|
role: Role object containing the modified role (None if deleted)
|
||||||
user: User object to load from
|
old_name: Old name if the name was changed
|
||||||
"""
|
|
||||||
|
|
||||||
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")
|
||||||
|
@ -95,16 +88,6 @@ class AuthPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
|
|
||||||
def modify_role(self, old_name: str, new_name: Optional[str]):
|
|
||||||
"""A call to this function indicated that a role was deleted (and has no users)
|
|
||||||
Might be used if modify_user is implemented.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
old_name: Name of the modified role
|
|
||||||
new_name: New role name or None if deleted
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_user(self, user, password):
|
def create_user(self, user, password):
|
||||||
"""If backend is using (writeable) external data, then create a new user on the external database.
|
"""If backend is using (writeable) external data, then create a new user on the external database.
|
||||||
|
|
||||||
|
@ -123,3 +106,24 @@ class AuthPlugin(Plugin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise MethodNotAllowed
|
raise MethodNotAllowed
|
||||||
|
|
||||||
|
def get_avatar(self, user) -> _Avatar:
|
||||||
|
"""Retrieve avatar for given user (if supported by auth backend)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: User to retrieve the avatar for
|
||||||
|
Raises:
|
||||||
|
NotFound: If no avatar found or not implemented
|
||||||
|
"""
|
||||||
|
raise NotFound
|
||||||
|
|
||||||
|
def set_avatar(self, user, avatar: _Avatar):
|
||||||
|
"""Set the avatar for given user (if supported by auth backend)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: User to set the avatar for
|
||||||
|
avatar: Avatar to set
|
||||||
|
Raises:
|
||||||
|
MethodNotAllowed: If not supported by Backend
|
||||||
|
"""
|
||||||
|
raise MethodNotAllowed
|
||||||
|
|
|
@ -7,11 +7,11 @@ from flask import current_app as app
|
||||||
from ldap3.utils.hashed import hashed
|
from ldap3.utils.hashed import hashed
|
||||||
from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError
|
from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError
|
||||||
from ldap3 import SUBTREE, MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE, HASHED_SALTED_MD5
|
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, NotFound
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.plugins import AuthPlugin, load_avatar_hook, save_avatar_hook
|
from flaschengeist.plugins import AuthPlugin, role_updated
|
||||||
from flaschengeist.models.user import User
|
from flaschengeist.models.user import User, Role, _Avatar
|
||||||
import flaschengeist.controller.userController as userController
|
import flaschengeist.controller.userController as userController
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,13 +43,9 @@ class AuthLDAP(AuthPlugin):
|
||||||
else:
|
else:
|
||||||
self.admin_dn = None
|
self.admin_dn = None
|
||||||
|
|
||||||
@load_avatar_hook
|
@role_updated
|
||||||
def load_avatar(avatar, user):
|
def _role_updated(role, old_name):
|
||||||
self.__load_avatar(avatar, user)
|
self.__modify_role(role, old_name)
|
||||||
|
|
||||||
@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:
|
||||||
|
@ -88,12 +84,12 @@ class AuthLDAP(AuthPlugin):
|
||||||
SUBTREE,
|
SUBTREE,
|
||||||
attributes=["uidNumber"],
|
attributes=["uidNumber"],
|
||||||
)
|
)
|
||||||
uidNumbers = sorted(
|
uid_number = (
|
||||||
self.ldap.response(),
|
sorted(self.ldap.response(), key=lambda i: i["attributes"]["uidNumber"], reverse=True,)[0][
|
||||||
key=lambda i: i["attributes"]["uidNumber"],
|
"attributes"
|
||||||
reverse=True,
|
]["uidNumber"]
|
||||||
|
+ 1
|
||||||
)
|
)
|
||||||
uidNumber = uidNumbers[0]["attributes"]["uidNumber"] + 1
|
|
||||||
dn = f"cn={user.firstname} {user.lastname},ou=user,{self.dn}"
|
dn = f"cn={user.firstname} {user.lastname},ou=user,{self.dn}"
|
||||||
object_class = [
|
object_class = [
|
||||||
"inetOrgPerson",
|
"inetOrgPerson",
|
||||||
|
@ -109,30 +105,13 @@ class AuthLDAP(AuthPlugin):
|
||||||
"loginShell": "/bin/bash",
|
"loginShell": "/bin/bash",
|
||||||
"uid": user.userid,
|
"uid": user.userid,
|
||||||
"userPassword": hashed(HASHED_SALTED_MD5, password),
|
"userPassword": hashed(HASHED_SALTED_MD5, password),
|
||||||
"uidNumber": uidNumber,
|
"uidNumber": uid_number,
|
||||||
}
|
}
|
||||||
ldap_conn.add(dn, object_class, attributes)
|
ldap_conn.add(dn, object_class, attributes)
|
||||||
self._set_roles(user)
|
self._set_roles(user)
|
||||||
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):
|
def modify_user(self, user: User, password=None, new_password=None):
|
||||||
try:
|
try:
|
||||||
dn = user.get_attribute("DN")
|
dn = user.get_attribute("DN")
|
||||||
|
@ -161,7 +140,7 @@ class AuthLDAP(AuthPlugin):
|
||||||
except (LDAPPasswordIsMandatoryError, LDAPBindError):
|
except (LDAPPasswordIsMandatoryError, LDAPBindError):
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
|
|
||||||
def __load_avatar(self, avatar, user):
|
def get_avatar(self, user):
|
||||||
self.ldap.connection.search(
|
self.ldap.connection.search(
|
||||||
"ou=user,{}".format(self.dn),
|
"ou=user,{}".format(self.dn),
|
||||||
"(uid={})".format(user.userid),
|
"(uid={})".format(user.userid),
|
||||||
|
@ -169,16 +148,21 @@ class AuthLDAP(AuthPlugin):
|
||||||
attributes=["uid", "jpegPhoto"],
|
attributes=["uid", "jpegPhoto"],
|
||||||
)
|
)
|
||||||
r = self.ldap.connection.response[0]["attributes"]
|
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 r["uid"][0] == user.userid:
|
||||||
|
avatar = _Avatar()
|
||||||
|
avatar.mimetype = "image/jpeg"
|
||||||
|
avatar.binary.extend(r["jpegPhoto"][0])
|
||||||
|
return avatar
|
||||||
|
else:
|
||||||
|
raise NotFound
|
||||||
|
|
||||||
|
def set_avatar(self, user, avatar: _Avatar):
|
||||||
if avatar.mimetype != "image/jpeg":
|
if avatar.mimetype != "image/jpeg":
|
||||||
# Try converting using Pillow (if installed)
|
# Try converting using Pillow (if installed)
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
image = Image.open(io.BytesIO(avatar.binary))
|
image = Image.open(io.BytesIO(avatar.binary))
|
||||||
image_bytes = io.BytesIO()
|
image_bytes = io.BytesIO()
|
||||||
image.save(image_bytes, format="JPEG")
|
image.save(image_bytes, format="JPEG")
|
||||||
|
@ -198,6 +182,27 @@ class AuthLDAP(AuthPlugin):
|
||||||
ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret)
|
ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret)
|
||||||
ldap_conn.modify(dn, {"jpegPhoto": [(MODIFY_REPLACE, [avatar.binary])]})
|
ldap_conn.modify(dn, {"jpegPhoto": [(MODIFY_REPLACE, [avatar.binary])]})
|
||||||
|
|
||||||
|
def __modify_role(
|
||||||
|
self,
|
||||||
|
role: Optional[Role],
|
||||||
|
old_name: 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 role:
|
||||||
|
ldap_conn.modify_dn(dn, f"cn={role.name}")
|
||||||
|
else:
|
||||||
|
ldap_conn.delete(dn)
|
||||||
|
|
||||||
|
except (LDAPPasswordIsMandatoryError, LDAPBindError):
|
||||||
|
raise BadRequest
|
||||||
|
|
||||||
def _get_groups(self, uid):
|
def _get_groups(self, uid):
|
||||||
groups = []
|
groups = []
|
||||||
self.ldap.connection.search(
|
self.ldap.connection.search(
|
||||||
|
@ -211,7 +216,7 @@ class AuthLDAP(AuthPlugin):
|
||||||
groups.append(data["attributes"]["cn"][0])
|
groups.append(data["attributes"]["cn"][0])
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
def _get_all_roles(self, ldap_conn):
|
def _get_all_roles(self):
|
||||||
self.ldap.connection.search(
|
self.ldap.connection.search(
|
||||||
f"ou=group,{self.dn}",
|
f"ou=group,{self.dn}",
|
||||||
"(cn=*)",
|
"(cn=*)",
|
||||||
|
@ -224,7 +229,7 @@ class AuthLDAP(AuthPlugin):
|
||||||
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_roles = self._get_all_roles(ldap_conn)
|
ldap_roles = self._get_all_roles()
|
||||||
|
|
||||||
gid_numbers = sorted(ldap_roles, key=lambda i: i["attributes"]["gidNumber"], reverse=True)
|
gid_numbers = sorted(ldap_roles, key=lambda i: i["attributes"]["gidNumber"], reverse=True)
|
||||||
gid_number = gid_numbers[0]["attributes"]["gidNumber"] + 1
|
gid_number = gid_numbers[0]["attributes"]["gidNumber"] + 1
|
||||||
|
@ -237,7 +242,7 @@ class AuthLDAP(AuthPlugin):
|
||||||
attributes={"gidNumber": gid_number},
|
attributes={"gidNumber": gid_number},
|
||||||
)
|
)
|
||||||
|
|
||||||
ldap_roles = self._get_all_roles(ldap_conn)
|
ldap_roles = self._get_all_roles()
|
||||||
|
|
||||||
for ldap_role in ldap_roles:
|
for ldap_role in ldap_roles:
|
||||||
if ldap_role["attributes"]["cn"][0] in user.roles:
|
if ldap_role["attributes"]["cn"][0] in user.roles:
|
||||||
|
|
|
@ -9,7 +9,7 @@ from flask import Blueprint, request, jsonify, make_response, Response
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed, NotFound
|
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, _Avatar
|
||||||
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
|
||||||
|
@ -97,8 +97,7 @@ def get_user(userid, current_session):
|
||||||
@users_bp.route("/users/<userid>/avatar", methods=["GET"])
|
@users_bp.route("/users/<userid>/avatar", methods=["GET"])
|
||||||
def get_avatar(userid):
|
def get_avatar(userid):
|
||||||
user = userController.get_user(userid)
|
user = userController.get_user(userid)
|
||||||
avatar = userController.Avatar()
|
avatar = userController.load_avatar(user)
|
||||||
userController.load_avatar(avatar, user)
|
|
||||||
if len(avatar.binary) > 0:
|
if len(avatar.binary) > 0:
|
||||||
response = Response(avatar.binary, mimetype=avatar.mimetype)
|
response = Response(avatar.binary, mimetype=avatar.mimetype)
|
||||||
response.add_etag()
|
response.add_etag()
|
||||||
|
@ -115,10 +114,10 @@ def set_avatar(userid, current_session):
|
||||||
|
|
||||||
file = request.files.get("file")
|
file = request.files.get("file")
|
||||||
if file:
|
if file:
|
||||||
avatar = userController.Avatar()
|
avatar = _Avatar()
|
||||||
avatar.mimetype = file.content_type
|
avatar.mimetype = file.content_type
|
||||||
avatar.binary = bytearray(file.stream.read())
|
avatar.binary = bytearray(file.stream.read())
|
||||||
userController.save_avatar(avatar, user)
|
userController.save_avatar(user, avatar)
|
||||||
return created()
|
return created()
|
||||||
else:
|
else:
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
|
|
Loading…
Reference in New Issue