From 712946983588ecb0f54a9ff4cbf647cf24db7047 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 29 Jul 2021 17:18:01 +0200 Subject: [PATCH 01/10] [roles] MySQL is caseinsensitive for strings so workaround it for renaming roles --- flaschengeist/controller/roleController.py | 6 +++--- flaschengeist/database.py | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/flaschengeist/controller/roleController.py b/flaschengeist/controller/roleController.py index a280388..1e4e6e1 100644 --- a/flaschengeist/controller/roleController.py +++ b/flaschengeist/controller/roleController.py @@ -2,7 +2,7 @@ from sqlalchemy.exc import IntegrityError from werkzeug.exceptions import BadRequest, NotFound from flaschengeist.models.user import Role, Permission -from flaschengeist.database import db +from flaschengeist.database import db, case_sensitive from flaschengeist import logger from flaschengeist.utils.hook import Hook @@ -36,8 +36,8 @@ def update_role(role, new_name): except IntegrityError: logger.debug("IntegrityError: Role might still be in use", exc_info=True) raise BadRequest("Role still in use") - elif role.name != new_name: - if db.session.query(db.exists().where(Role.name == new_name)).scalar(): + else: + if role.name == new_name or db.session.query(db.exists().where(Role.name == case_sensitive(new_name))).scalar(): raise BadRequest("Name already used") role.name = new_name db.session.commit() diff --git a/flaschengeist/database.py b/flaschengeist/database.py index f0b13d6..85fc3e1 100644 --- a/flaschengeist/database.py +++ b/flaschengeist/database.py @@ -1,3 +1,10 @@ from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() + + +def case_sensitive(s): + if db.session.bind.dialect.name == "mysql": + from sqlalchemy import func + return func.binary(s) + return s From 0d1a39f217428258feeac1f643443c535306f0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 17 Apr 2021 20:37:53 +0200 Subject: [PATCH 02/10] [balance] add sorting of transaction --- flaschengeist/plugins/balance/balance_controller.py | 9 +++++++-- flaschengeist/plugins/balance/routes.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/flaschengeist/plugins/balance/balance_controller.py b/flaschengeist/plugins/balance/balance_controller.py index 0ab282f..58b1152 100644 --- a/flaschengeist/plugins/balance/balance_controller.py +++ b/flaschengeist/plugins/balance/balance_controller.py @@ -113,7 +113,9 @@ def get_transaction(transaction_id) -> Transaction: return transaction -def get_transactions(user, start=None, end=None, limit=None, offset=None, show_reversal=False, show_cancelled=True): +def get_transactions( + user, start=None, end=None, limit=None, offset=None, show_reversal=False, show_cancelled=True, descending=False +): count = None query = Transaction.query.filter((Transaction.sender_ == user) | (Transaction.receiver_ == user)) if start: @@ -125,7 +127,10 @@ def get_transactions(user, start=None, end=None, limit=None, offset=None, show_r query = query.filter(Transaction.original_ == None) if not show_cancelled: query = query.filter(Transaction.reversal_id.is_(None)) - query = query.order_by(Transaction.time) + if descending: + query = query.order_by(Transaction.time.desc()) + else: + query = query.order_by(Transaction.time) if limit is not None: count = query.count() query = query.limit(limit) diff --git a/flaschengeist/plugins/balance/routes.py b/flaschengeist/plugins/balance/routes.py index d28f913..1341e6e 100644 --- a/flaschengeist/plugins/balance/routes.py +++ b/flaschengeist/plugins/balance/routes.py @@ -170,6 +170,7 @@ def get_transactions(userid, current_session: Session): show_cancelled = request.args.get("showCancelled", True) limit = request.args.get("limit") offset = request.args.get("offset") + descending = request.args.get("descending", False) try: if limit is not None: limit = int(limit) @@ -179,11 +180,20 @@ def get_transactions(userid, current_session: Session): show_reversals = str2bool(show_reversals) if not isinstance(show_cancelled, bool): show_cancelled = str2bool(show_cancelled) + if not isinstance(descending, bool): + descending = str2bool(descending) except ValueError: raise BadRequest transactions, count = balance_controller.get_transactions( - user, start, end, limit, offset, show_reversal=show_reversals, show_cancelled=show_cancelled + user, + start, + end, + limit, + offset, + show_reversal=show_reversals, + show_cancelled=show_cancelled, + descending=descending, ) return {"transactions": transactions, "count": count} From 4e46ea1ca3b0743375b6adab05bf127b8e940ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 17 Apr 2021 22:05:49 +0200 Subject: [PATCH 03/10] [balance] add get and modify limits for all users --- flaschengeist/plugins/balance/routes.py | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/flaschengeist/plugins/balance/routes.py b/flaschengeist/plugins/balance/routes.py index 1341e6e..b0ce3c9 100644 --- a/flaschengeist/plugins/balance/routes.py +++ b/flaschengeist/plugins/balance/routes.py @@ -99,6 +99,32 @@ def set_limit(userid, current_session: Session): return HTTP.no_content() +@BalancePlugin.blueprint.route("/users/balance/limit", methods=["GET", "PUT"]) +@login_required(permission=permissions.SET_LIMIT) +def limits(current_session: Session): + """Get, Modify limit of all users + + Args: + current_ession: Session sent with Authorization Header + + Returns: + JSON encoded array of userid with limit or HTTP-error + """ + + users = userController.get_users() + if request.method == "GET": + return jsonify([{"userid": user.userid, "limit": user.get_attribute("balance_limit")} for user in users]) + + data = request.get_json() + try: + limit = data["limit"] + except (TypeError, KeyError): + raise BadRequest + for user in users: + balance_controller.set_limit(user, limit) + return HTTP.no_content() + + @BalancePlugin.blueprint.route("/users//balance", methods=["GET"]) @login_required(permission=permissions.SHOW) def get_balance(userid, current_session: Session): From cadde543f214a94d709ce3bf5a84fdbc6336b153 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 30 Aug 2021 14:39:54 +0200 Subject: [PATCH 04/10] [plugins] Improved handling of plugin loading errors --- flaschengeist/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flaschengeist/app.py b/flaschengeist/app.py index e91f000..0012c6f 100644 --- a/flaschengeist/app.py +++ b/flaschengeist/app.py @@ -54,6 +54,8 @@ def __load_plugins(app): logger.error( f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.", exc_info=True ) + del plugin + continue if isinstance(plugin, AuthPlugin): logger.debug(f"Found authentication plugin: {entry_point.name}") if entry_point.name == config["FLASCHENGEIST"]["auth"]: @@ -65,6 +67,7 @@ def __load_plugins(app): app.config["FG_PLUGINS"][entry_point.name] = plugin if "FG_AUTH_BACKEND" not in app.config: logger.error("No authentication plugin configured or authentication plugin not found") + raise RuntimeError("No authentication plugin configured or authentication plugin not found") def install_all(): From 2dabd1dd34de907c801809eedc1216817604249c Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 11 Nov 2021 12:23:45 +0100 Subject: [PATCH 05/10] [events] Fix deleteing an event --- flaschengeist/plugins/events/event_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flaschengeist/plugins/events/event_controller.py b/flaschengeist/plugins/events/event_controller.py index 31a3b7b..3211363 100644 --- a/flaschengeist/plugins/events/event_controller.py +++ b/flaschengeist/plugins/events/event_controller.py @@ -116,7 +116,7 @@ def get_event(event_id, with_backup=False) -> Event: if event is None: raise NotFound if not with_backup: - return clear_backup(event) + clear_backup(event) return event @@ -154,6 +154,7 @@ def delete_event(event_id): NotFound if not found """ event = get_event(event_id) + logger.info(f"{type(event)} {event.__str__}") db.session.delete(event) db.session.commit() From 3d833fb6af4b8ac3f4eef696d924c43f56650cc7 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 11 Nov 2021 15:22:15 +0100 Subject: [PATCH 06/10] [plugin] Plugins should have an unique ID --- flaschengeist/plugins/__init__.py | 3 ++- flaschengeist/plugins/events/__init__.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index 094eae7..fabc4e2 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -33,6 +33,7 @@ class Plugin: blueprint = None # You have to override permissions = [] # You have to override + id = "dev.flaschengeist.plugin" # You have to override name = "plugin" # You have to override models = None # You have to override @@ -94,7 +95,7 @@ class Plugin: db.session.commit() def notify(self, user, text: str, data=None): - n = Notification(text=text, data=data, plugin=self.name, user_=user) + n = Notification(text=text, data=data, plugin=self.id, user_=user) db.session.add(n) db.session.commit() diff --git a/flaschengeist/plugins/events/__init__.py b/flaschengeist/plugins/events/__init__.py index e4a0af0..c86a214 100644 --- a/flaschengeist/plugins/events/__init__.py +++ b/flaschengeist/plugins/events/__init__.py @@ -11,6 +11,7 @@ from . import permissions, models class EventPlugin(Plugin): name = "events" + id = "dev.flaschengeist.events" plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.name]) permissions = permissions.permissions blueprint = Blueprint(name, __name__) From f7e07fdadec4ffdd0490ff08a5dfd033e7e1badc Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 11 Nov 2021 15:22:55 +0100 Subject: [PATCH 07/10] [events] Hotfix: delete an event with registered jobs --- .../plugins/events/event_controller.py | 42 +++++++++++++------ flaschengeist/plugins/events/routes.py | 5 ++- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/flaschengeist/plugins/events/event_controller.py b/flaschengeist/plugins/events/event_controller.py index 3211363..1bc8b43 100644 --- a/flaschengeist/plugins/events/event_controller.py +++ b/flaschengeist/plugins/events/event_controller.py @@ -153,8 +153,9 @@ def delete_event(event_id): Raises: NotFound if not found """ - event = get_event(event_id) - logger.info(f"{type(event)} {event.__str__}") + event = get_event(event_id, True) + for job in event.jobs: + delete_job(job) db.session.delete(event) db.session.commit() @@ -202,25 +203,42 @@ def update(): def delete_job(job: Job): + for service in job.services: + unassign_job(service=service, notify=True) db.session.delete(job) db.session.commit() -def assign_to_job(job: Job, user, value): +def assign_job(job: Job, user, value): + assert value > 0 service = Service.query.get((job.id, user.id_)) - if value < 0: - if not service: - raise BadRequest - db.session.delete(service) + if service: + service.value = value else: - if service: - service.value = value - else: - service = Service(user_=user, value=value, job_=job) - db.session.add(service) + service = Service(user_=user, value=value, job_=job) + db.session.add(service) db.session.commit() +def unassign_job(job: Job = None, user=None, service=None, notify=False): + if service is None: + assert(job is not None and user is not None) + service = Service.query.get((job.id, user.id_)) + else: + user = service.user_ + if not service: + raise BadRequest + + event_id = service.job_.event_id_ + + db.session.delete(service) + db.session.commit() + if notify: + EventPlugin.plugin.notify( + user, "Your assignmet was cancelled", {"event_id": event_id} + ) + + @scheduled def assign_backups(): logger.debug("Notifications") diff --git a/flaschengeist/plugins/events/routes.py b/flaschengeist/plugins/events/routes.py index 7fccccf..fce9416 100644 --- a/flaschengeist/plugins/events/routes.py +++ b/flaschengeist/plugins/events/routes.py @@ -415,7 +415,10 @@ def update_job(event_id, job_id, current_session: Session): user != current_session.user_ and not current_session.user_.has_permission(permissions.ASSIGN_OTHER) ): raise Forbidden - event_controller.assign_to_job(job, user, value) + if value > 0: + event_controller.assign_job(job, user, value) + else: + event_controller.unassign_job(job, user, notify=user != current_session.user_) except (KeyError, ValueError): raise BadRequest From 4e1799e29733f30fc33e1df5c5e1ed4e4dbc24fa Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 29 Jul 2021 15:50:03 +0200 Subject: [PATCH 08/10] [auth_ldap] Allow more configuration * Allow configuring the password hash (SSHA, PBKDF2 or Argon2) * Allow setting custom dn templates for users and groups to e.g. allow "ou=people" or "ou=user" * Allow setting custom object class for entries * Stop using deprecated openssl constants --- flaschengeist/flaschengeist.toml | 21 ++- flaschengeist/models/user.py | 2 +- flaschengeist/plugins/auth_ldap/__init__.py | 163 +++++++++++--------- setup.py | 7 +- 4 files changed, 107 insertions(+), 86 deletions(-) diff --git a/flaschengeist/flaschengeist.toml b/flaschengeist/flaschengeist.toml index ffbb51d..a920eb6 100644 --- a/flaschengeist/flaschengeist.toml +++ b/flaschengeist/flaschengeist.toml @@ -29,17 +29,16 @@ level = "WARNING" [auth_plain] enabled = true -#[auth_ldap] -# enabled = true -# host = -# port = -# bind_dn = -# base_dn = -# secret = -# use_ssl = -# admin_dn = -# admin_dn = -# default_gid = +[auth_ldap] +# Full documentation https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/plugins_auth_ldap +enabled = false +# host = "localhost" +# port = 389 +# base_dn = "dc=example,dc=com" +# root_dn = "cn=Manager,dc=example,dc=com" +# root_secret = "SuperS3cret" +# Uncomment to use secured LDAP (ldaps) +# use_ssl = true [MESSAGES] welcome_subject = "Welcome to Flaschengeist {name}" diff --git a/flaschengeist/models/user.py b/flaschengeist/models/user.py index 43ef91e..84116a6 100644 --- a/flaschengeist/models/user.py +++ b/flaschengeist/models/user.py @@ -56,7 +56,7 @@ class User(db.Model, ModelSerializeMixin): display_name: str = db.Column(db.String(30)) 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) + mail: str = db.Column(db.String(60)) birthday: Optional[date] = db.Column(db.Date) roles: list[str] = [] permissions: Optional[list[str]] = None diff --git a/flaschengeist/plugins/auth_ldap/__init__.py b/flaschengeist/plugins/auth_ldap/__init__.py index c3ae7c6..f8c6d00 100644 --- a/flaschengeist/plugins/auth_ldap/__init__.py +++ b/flaschengeist/plugins/auth_ldap/__init__.py @@ -1,12 +1,12 @@ """LDAP Authentication Provider Plugin""" import io +import os import ssl from typing import Optional 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 ldap3 import SUBTREE, MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from flaschengeist import logger @@ -16,32 +16,36 @@ import flaschengeist.controller.userController as userController class AuthLDAP(AuthPlugin): - def __init__(self, cfg): + def __init__(self, config): super().__init__() - config = {"port": 389, "use_ssl": False} - config.update(cfg) app.config.update( - LDAP_SERVER=config["host"], - LDAP_PORT=config["port"], - LDAP_BINDDN=config["bind_dn"], + LDAP_SERVER=config.get("host", "localhost"), + LDAP_PORT=config.get("port", 389), + LDAP_BINDDN=config.get("bind_dn", None), + LDAP_SECRET=config.get("secret", None), + LDAP_USE_SSL=config.get("use_ssl", False), + # That's not TLS, its dirty StartTLS on unencrypted LDAP LDAP_USE_TLS=False, - LDAP_USE_SSL=config["use_ssl"], - LDAP_TLS_VERSION=ssl.PROTOCOL_TLSv1_2, - LDAP_REQUIRE_CERT=ssl.CERT_NONE, + LDAP_TLS_VERSION=ssl.PROTOCOL_TLS, FORCE_ATTRIBUTE_VALUE_AS_LIST=True, ) - if "secret" in config: - app.config["LDAP_SECRET"] = config["secret"] + logger.warning(app.config.get("LDAP_USE_SSL")) + if "ca_cert" in config: + app.config["LDAP_CA_CERTS_FILE"] = config["ca_cert"] + else: + # Default is CERT_REQUIRED + app.config["LDAP_REQUIRE_CERT"] = ssl.CERT_OPTIONAL self.ldap = LDAPConn(app) - self.dn = config["base_dn"] - self.default_gid = config["default_gid"] + self.base_dn = config["base_dn"] + self.search_dn = config.get("search_dn", "ou=people,{base_dn}").format(base_dn=self.base_dn) + self.group_dn = config.get("group_dn", "ou=group,{base_dn}").format(base_dn=self.base_dn) + self.password_hash = config.get("password_hash", "SSHA").upper() + self.object_classes = config.get("object_classes", ["inetOrgPerson"]) + self.user_attributes: dict = config.get("user_attributes", {}) # TODO: might not be set if modify is called - if "admin_dn" in config: - self.admin_dn = config["admin_dn"] - self.admin_secret = config["admin_secret"] - else: - self.admin_dn = None + self.root_dn = config.get("root_dn", None) + self.root_secret = self.root_dn = config.get("root_secret", None) @after_role_updated def _role_updated(role, new_name): @@ -50,7 +54,7 @@ class AuthLDAP(AuthPlugin): def login(self, user, password): if not user: return False - return self.ldap.authenticate(user.userid, password, "uid", self.dn) + return self.ldap.authenticate(user.userid, password, "uid", self.base_dn) def find_user(self, userid, mail=None): attr = self.__find(userid, mail) @@ -64,42 +68,41 @@ class AuthLDAP(AuthPlugin): self.__update(user, attr) def create_user(self, user, password): - if self.admin_dn is None: - logger.error("admin_dn missing in ldap config!") + if self.root_dn is None: + logger.error("root_dn missing in ldap config!") raise InternalServerError try: - ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) - self.ldap.connection.search( - "ou=user,{}".format(self.dn), - "(uidNumber=*)", - SUBTREE, - attributes=["uidNumber"], + ldap_conn = self.ldap.connect(self.root_dn, self.root_secret) + attributes = self.user_attributes.copy() + if "uidNumber" in attributes: + self.ldap.connection.search( + self.search_dn, + "(uidNumber=*)", + SUBTREE, + attributes=["uidNumber"], + ) + resp = sorted( + self.ldap.response(), + key=lambda i: i["attributes"]["uidNumber"], + reverse=True, + ) + attributes = resp[0]["attributes"]["uidNumber"] + 1 if resp else attributes["uidNumber"] + dn = self.dn_template.format( + firstname=user.firstname, + lastname=user.lastname, + userid=user.userid, + mail=user.mail, + display_name=user.display_name, + base_dn=self.base_dn, ) - uid_number = ( - sorted(self.ldap.response(), key=lambda i: i["attributes"]["uidNumber"], reverse=True,)[0][ - "attributes" - ]["uidNumber"] - + 1 - ) - dn = f"cn={user.firstname} {user.lastname},ou=user,{self.dn}" - object_class = [ - "inetOrgPerson", - "posixAccount", - "person", - "organizationalPerson", - ] - attributes = { - "sn": user.firstname, - "givenName": user.lastname, - "gidNumber": self.default_gid, - "homeDirectory": f"/home/{user.userid}", - "loginShell": "/bin/bash", + attributes.update({ + "sn": user.lastname, + "givenName": user.firstname, "uid": user.userid, - "userPassword": hashed(HASHED_SALTED_MD5, password), - "uidNumber": uid_number, - } - ldap_conn.add(dn, object_class, attributes) + "userPassword": self.__hash(password), + }) + ldap_conn.add(dn, self.object_classes, attributes) self._set_roles(user) except (LDAPPasswordIsMandatoryError, LDAPBindError): raise BadRequest @@ -110,10 +113,10 @@ class AuthLDAP(AuthPlugin): if password: ldap_conn = self.ldap.connect(dn, password) else: - if self.admin_dn is None: - logger.error("admin_dn missing in ldap config!") + if self.root_dn is None: + logger.error("root_dn missing in ldap config!") raise InternalServerError - ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) + ldap_conn = self.ldap.connect(self.root_dn, self.root_secret) modifier = {} for name, ldap_name in [ ("firstname", "givenName"), @@ -124,9 +127,7 @@ class AuthLDAP(AuthPlugin): 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])] + modifier["userPassword"] = [(MODIFY_REPLACE, [self.__hash(new_password)])] ldap_conn.modify(dn, modifier) self._set_roles(user) except (LDAPPasswordIsMandatoryError, LDAPBindError): @@ -134,7 +135,7 @@ class AuthLDAP(AuthPlugin): def get_avatar(self, user): self.ldap.connection.search( - "ou=user,{}".format(self.dn), + self.search_dn, "(uid={})".format(user.userid), SUBTREE, attributes=["jpegPhoto"], @@ -150,8 +151,8 @@ class AuthLDAP(AuthPlugin): raise NotFound def set_avatar(self, user, avatar: _Avatar): - if self.admin_dn is None: - logger.error("admin_dn missing in ldap config!") + if self.root_dn is None: + logger.error("root_dn missing in ldap config!") raise InternalServerError if avatar.mimetype != "image/jpeg": @@ -172,16 +173,16 @@ class AuthLDAP(AuthPlugin): raise BadRequest("Unsupported image format") dn = user.get_attribute("DN") - ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) + ldap_conn = self.ldap.connect(self.root_dn, self.root_secret) ldap_conn.modify(dn, {"jpegPhoto": [(MODIFY_REPLACE, [avatar.binary])]}) def __find(self, userid, mail=None): """Find attributes of an user by uid or mail in LDAP""" con = self.ldap.connection if not con: - con = self.ldap.connect(self.admin_dn, self.admin_secret) + con = self.ldap.connect(self.root_dn, self.root_secret) con.search( - f"ou=user,{self.dn}", + self.search_dn, f"(| (uid={userid})(mail={mail}))" if mail else f"(uid={userid})", SUBTREE, attributes=["uid", "givenName", "sn", "mail"], @@ -205,12 +206,12 @@ class AuthLDAP(AuthPlugin): role: Role, new_name: Optional[str], ): - if self.admin_dn is None: - logger.error("admin_dn missing in ldap config!") + if self.root_dn is None: + logger.error("root_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={role.name})", SUBTREE, attributes=["cn"]) + ldap_conn = self.ldap.connect(self.root_dn, self.root_secret) + ldap_conn.search(self.group_dn, f"(cn={role.name})", SUBTREE, attributes=["cn"]) if len(ldap_conn.response) > 0: dn = ldap_conn.response[0]["dn"] if new_name: @@ -221,10 +222,27 @@ class AuthLDAP(AuthPlugin): except (LDAPPasswordIsMandatoryError, LDAPBindError): raise BadRequest + def __hash(self, password): + if self.password_hash == "ARGON2": + from argon2 import PasswordHasher + + return f"{{ARGON2}}{PasswordHasher().hash(password)}" + else: + from hashlib import pbkdf2_hmac, sha1 + import base64 + + salt = os.urandom(16) + if self.password_hash == "PBKDF2": + rounds = 200000 + password_hash = base64.b64encode(pbkdf2_hmac("sha512", password.encode("utf-8"), salt, rounds)).decode() + return f"{{PBKDF2-SHA512}}{rounds}${base64.b64encode(salt).decode()}${password_hash}" + else: + return f"{{SSHA}}{base64.b64encode(sha1(password + salt) + salt)}" + def _get_groups(self, uid): groups = [] self.ldap.connection.search( - "ou=group,{}".format(self.dn), + self.group_dn, "(memberUID={})".format(uid), SUBTREE, attributes=["cn"], @@ -236,7 +254,7 @@ class AuthLDAP(AuthPlugin): def _get_all_roles(self): self.ldap.connection.search( - f"ou=group,{self.dn}", + self.group_dn, "(cn=*)", SUBTREE, attributes=["cn", "gidNumber", "memberUid"], @@ -245,8 +263,7 @@ class AuthLDAP(AuthPlugin): def _set_roles(self, user: User): try: - ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) - + ldap_conn = self.ldap.connect(self.root_dn, self.root_secret) ldap_roles = self._get_all_roles() gid_numbers = sorted(ldap_roles, key=lambda i: i["attributes"]["gidNumber"], reverse=True) @@ -255,7 +272,7 @@ class AuthLDAP(AuthPlugin): for user_role in user.roles: if user_role not in [role["attributes"]["cn"][0] for role in ldap_roles]: ldap_conn.add( - f"cn={user_role},ou=group,{self.dn}", + f"cn={user_role},{self.group_dn}", ["posixGroup"], attributes={"gidNumber": gid_number}, ) diff --git a/setup.py b/setup.py index d7bfd18..7860856 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,12 @@ setup( "werkzeug", mysql_driver, ], - extras_require={"ldap": ["flask_ldapconn", "ldap3"], "pricelist": ["pillow"], "test": ["pytest", "coverage"]}, + extras_require={ + "ldap": ["flask_ldapconn", "ldap3"], + "argon": ["argon2-cffi"], + "pricelist": ["pillow"], + "test": ["pytest", "coverage"], + }, entry_points={ "flaschengeist.plugin": [ # Authentication providers From f80ad5c420a9c41f558359a39188993a78ac49b4 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 29 Jul 2021 16:45:26 +0200 Subject: [PATCH 09/10] [auth_ldap] Fix typo in __init__ --- flaschengeist/plugins/auth_ldap/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flaschengeist/plugins/auth_ldap/__init__.py b/flaschengeist/plugins/auth_ldap/__init__.py index f8c6d00..32799e4 100644 --- a/flaschengeist/plugins/auth_ldap/__init__.py +++ b/flaschengeist/plugins/auth_ldap/__init__.py @@ -45,7 +45,7 @@ class AuthLDAP(AuthPlugin): # TODO: might not be set if modify is called self.root_dn = config.get("root_dn", None) - self.root_secret = self.root_dn = config.get("root_secret", None) + self.root_secret = config.get("root_secret", None) @after_role_updated def _role_updated(role, new_name): @@ -219,8 +219,11 @@ class AuthLDAP(AuthPlugin): else: ldap_conn.delete(dn) - except (LDAPPasswordIsMandatoryError, LDAPBindError): + except LDAPPasswordIsMandatoryError: raise BadRequest + except LDAPBindError: + logger.debug(f"Could not bind to LDAP server", exc_info=True) + raise InternalServerError def __hash(self, password): if self.password_hash == "ARGON2": From 80f06e483b5418c8bf29670e80222ed58f85f946 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 29 Jul 2021 17:10:52 +0200 Subject: [PATCH 10/10] [auth_ldap] modify_role has to be called before the update to change it on the backend --- flaschengeist/plugins/auth_ldap/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flaschengeist/plugins/auth_ldap/__init__.py b/flaschengeist/plugins/auth_ldap/__init__.py index 32799e4..627325a 100644 --- a/flaschengeist/plugins/auth_ldap/__init__.py +++ b/flaschengeist/plugins/auth_ldap/__init__.py @@ -10,7 +10,7 @@ from ldap3 import SUBTREE, MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from flaschengeist import logger -from flaschengeist.plugins import AuthPlugin, after_role_updated +from flaschengeist.plugins import AuthPlugin, before_role_updated from flaschengeist.models.user import User, Role, _Avatar import flaschengeist.controller.userController as userController @@ -47,8 +47,9 @@ class AuthLDAP(AuthPlugin): self.root_dn = config.get("root_dn", None) self.root_secret = config.get("root_secret", None) - @after_role_updated + @before_role_updated def _role_updated(role, new_name): + logger.debug(f"LDAP: before_role_updated called with ({role}, {new_name})") self.__modify_role(role, new_name) def login(self, user, password):