Compare commits
	
		
			9 Commits
		
	
	
		
			193ffeff9d
			...
			b40d40644d
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | b40d40644d | |
|  | 319889ee43 | |
|  | 4be7cccadb | |
|  | 9077c9fd11 | |
|  | d7428b2ed1 | |
|  | 5bab4a7cde | |
|  | d8028c4681 | |
|  | 8b15a45902 | |
|  | ae583a6d18 | 
|  | @ -89,6 +89,7 @@ def main(*args, **kwargs): | |||
|     from .docs_cmd import docs | ||||
|     from .run_cmd import run | ||||
|     from .install_cmd import install | ||||
|     from .docker_cmd import docker | ||||
| 
 | ||||
|     # Override logging level | ||||
|     environ.setdefault("FG_LOGGING", logging.getLevelName(LOGGING_MAX)) | ||||
|  | @ -98,4 +99,5 @@ def main(*args, **kwargs): | |||
|     cli.add_command(install) | ||||
|     cli.add_command(plugin) | ||||
|     cli.add_command(run) | ||||
|     cli.add_command(docker) | ||||
|     cli(*args, **kwargs) | ||||
|  |  | |||
|  | @ -0,0 +1,54 @@ | |||
| import click | ||||
| from click.decorators import pass_context | ||||
| from flask.cli import with_appcontext | ||||
| from os import environ | ||||
| 
 | ||||
| from flaschengeist import logger | ||||
| from flaschengeist.controller import pluginController | ||||
| from werkzeug.exceptions import NotFound | ||||
| import traceback | ||||
| 
 | ||||
| 
 | ||||
| @click.group() | ||||
| def docker(): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| @docker.command() | ||||
| @with_appcontext | ||||
| @pass_context | ||||
| def setup(ctx): | ||||
|     """Setup flaschengesit in docker container""" | ||||
|     click.echo("Setup docker") | ||||
| 
 | ||||
|     plugins = environ.get("FG_ENABLE_PLUGINS") | ||||
| 
 | ||||
|     if not plugins: | ||||
|         click.secho("no evironment variable is set for 'FG_ENABLE_PLUGINS'", fg="yellow") | ||||
|         click.secho("set 'FG_ENABLE_PLUGINS' to 'auth_ldap', 'mail', 'balance', 'pricelist_old', 'events'") | ||||
|         plugins = ("auth_ldap", "mail", "pricelist_old", "events", "balance") | ||||
|     else: | ||||
|         plugins = plugins.split(" ") | ||||
| 
 | ||||
|     print(plugins) | ||||
| 
 | ||||
|     for name in plugins: | ||||
|         click.echo(f"Installing {name}{'.'*(20-len(name))}", nl=False) | ||||
|         try: | ||||
|             pluginController.install_plugin(name) | ||||
|         except Exception as e: | ||||
|             click.secho(" failed", fg="red") | ||||
|             if logger.getEffectiveLevel() > 10: | ||||
|                 ctx.fail(f"[{e.__class__.__name__}] {e}") | ||||
|             else: | ||||
|                 ctx.fail(traceback.format_exc()) | ||||
|         else: | ||||
|             click.secho(" ok", fg="green") | ||||
| 
 | ||||
|     for name in plugins: | ||||
|         click.echo(f"Enabling {name}{'.'*(20-len(name))}", nl=False) | ||||
|         try: | ||||
|             pluginController.enable_plugin(name) | ||||
|             click.secho(" ok", fg="green") | ||||
|         except NotFound: | ||||
|             click.secho(" not installed / not found", fg="red") | ||||
|  | @ -108,9 +108,12 @@ def install_plugin(plugin_name: str): | |||
|     plugin.install() | ||||
|     # Check migrations | ||||
|     directory = entry_point[0].dist.locate_file("") | ||||
|     logger.debug(f"Checking for migrations in {directory}") | ||||
|     for loc in entry_point[0].module.split(".") + ["migrations"]: | ||||
|         directory /= loc | ||||
|         logger.debug(f"Checking for migrations with loc in {directory}") | ||||
|     if directory.exists(): | ||||
|         logger.debug(f"Found migrations in {directory}") | ||||
|         database_upgrade(revision=f"{plugin_name}@head") | ||||
|     db.session.commit() | ||||
|     return plugin | ||||
|  |  | |||
|  | @ -1,11 +1,14 @@ | |||
| import re | ||||
| import secrets | ||||
| import hashlib | ||||
| 
 | ||||
| from io import BytesIO | ||||
| from typing import Optional | ||||
| from typing import Optional, Union | ||||
| from flask import make_response | ||||
| from flask.json import provider | ||||
| from sqlalchemy import exc | ||||
| from sqlalchemy_utils import merge_references | ||||
| from datetime import datetime, timedelta, timezone | ||||
| from datetime import datetime, timedelta, timezone, date | ||||
| from flask.helpers import send_file | ||||
| from werkzeug.exceptions import NotFound, BadRequest, Forbidden | ||||
| 
 | ||||
|  | @ -16,7 +19,12 @@ from ..models import Notification, User, Role | |||
| from ..models.user import _PasswordReset | ||||
| from ..utils.hook import Hook | ||||
| from ..utils.datetime import from_iso_format | ||||
| from ..controller import imageController, messageController, pluginController, sessionController | ||||
| from ..controller import ( | ||||
|     imageController, | ||||
|     messageController, | ||||
|     pluginController, | ||||
|     sessionController, | ||||
| ) | ||||
| from ..plugins import AuthPlugin | ||||
| 
 | ||||
| 
 | ||||
|  | @ -195,7 +203,11 @@ def delete_user(user: User): | |||
|         deleted_user = get_user("__deleted_user__", True) | ||||
|     except NotFound: | ||||
|         deleted_user = User( | ||||
|             userid="__deleted_user__", firstname="USER", lastname="DELETED", display_name="DELETED USER", deleted=True | ||||
|             userid="__deleted_user__", | ||||
|             firstname="USER", | ||||
|             lastname="DELETED", | ||||
|             display_name="DELETED USER", | ||||
|             deleted=True, | ||||
|         ) | ||||
|         db.session.add(user) | ||||
|         db.session.flush() | ||||
|  | @ -206,7 +218,10 @@ def delete_user(user: User): | |||
|         db.session.delete(user) | ||||
|         db.session.commit() | ||||
|     except exc.IntegrityError: | ||||
|         logger.error("Delete of user failed, there might be ForeignKey contraits from disabled plugins", exec_info=True) | ||||
|         logger.error( | ||||
|             "Delete of user failed, there might be ForeignKey contraits from disabled plugins", | ||||
|             exec_info=True, | ||||
|         ) | ||||
|         # Remove at least all personal data | ||||
|         user.userid = f"__deleted_user__{user.id_}" | ||||
|         user.display_name = "DELETED USER" | ||||
|  | @ -228,7 +243,10 @@ def register(data, passwd=None): | |||
|     values = {key: value for key, value in data.items() if key in allowed_keys} | ||||
|     roles = values.pop("roles", []) | ||||
|     if "birthday" in data: | ||||
|         values["birthday"] = from_iso_format(data["birthday"]).date() | ||||
|         if isinstance(data["birthday"], date): | ||||
|             values["birthday"] = data["birthday"] | ||||
|         else: | ||||
|             values["birthday"] = from_iso_format(data["birthday"]).date() | ||||
|     if "mail" in data and not re.match(r"[^@]+@[^@]+\.[^@]+", data["mail"]): | ||||
|         raise BadRequest("Invalid mail given") | ||||
|     user = User(**values) | ||||
|  | @ -262,14 +280,22 @@ def register(data, passwd=None): | |||
|     return user | ||||
| 
 | ||||
| 
 | ||||
| def load_avatar(user: User): | ||||
| def get_last_modified(user: User): | ||||
|     """Get the last modification date of the user""" | ||||
|     return get_provider(user.userid).get_last_modified(user) | ||||
| 
 | ||||
| 
 | ||||
| def load_avatar(user: User, etag: Union[str, None] = None): | ||||
|     if user.avatar_ is not None: | ||||
|         return imageController.send_image(image=user.avatar_) | ||||
|     else: | ||||
|         provider = get_provider(user.userid) | ||||
|         avatar = provider.get_avatar(user) | ||||
|         new_etag = hashlib.md5(avatar.binary).hexdigest() | ||||
|         if new_etag == etag: | ||||
|             return make_response("", 304) | ||||
|         if len(avatar.binary) > 0: | ||||
|             return send_file(BytesIO(avatar.binary), avatar.mimetype) | ||||
|             return send_file(BytesIO(avatar.binary), avatar.mimetype, etag=new_etag) | ||||
|     raise NotFound | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,6 @@ | |||
| from __future__ import annotations  # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered) | ||||
| from __future__ import ( | ||||
|     annotations, | ||||
| )  # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered) | ||||
| 
 | ||||
| from typing import Optional, Union, List | ||||
| from datetime import date, datetime | ||||
|  | @ -45,7 +47,7 @@ class User(db.Model, ModelSerializeMixin): | |||
| 
 | ||||
|     Attributes: | ||||
|         id: Id in Database as Primary Key. | ||||
|         uid: User ID used by authentication provider | ||||
|         userid: User ID used by authentication provider | ||||
|         display_name: Name to show | ||||
|         firstname: Firstname of the User | ||||
|         lastname: Lastname of the User | ||||
|  |  | |||
|  | @ -248,6 +248,16 @@ class AuthPlugin(Plugin): | |||
|         """ | ||||
|         raise NotImplementedError | ||||
| 
 | ||||
|     def get_modified_time(self, user): | ||||
|         """If backend is using external data, then return the timestamp of the last modification | ||||
| 
 | ||||
|         Args: | ||||
|             user: User object | ||||
|         Returns: | ||||
|             Timestamp of last modification | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     def get_avatar(self, user) -> _Avatar: | ||||
|         """Retrieve avatar for given user (if supported by auth backend) | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,6 +10,8 @@ from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError | |||
| from werkzeug.exceptions import BadRequest, InternalServerError, NotFound | ||||
| from werkzeug.datastructures import FileStorage | ||||
| 
 | ||||
| from datetime import datetime | ||||
| 
 | ||||
| from flaschengeist import logger | ||||
| from flaschengeist.config import config | ||||
| from flaschengeist.controller import userController | ||||
|  | @ -126,9 +128,12 @@ class AuthLDAP(AuthPlugin): | |||
|     def modify_user(self, user: User, password=None, new_password=None): | ||||
|         try: | ||||
|             dn = user.get_attribute("DN") | ||||
|             logger.debug(f"LDAP: modify_user for user {user.userid} with dn {dn}") | ||||
|             if password: | ||||
|                 logger.debug(f"LDAP: modify_user for user {user.userid} with password") | ||||
|                 ldap_conn = self.ldap.connect(dn, password) | ||||
|             else: | ||||
|                 logger.debug(f"LDAP: modify_user for user {user.userid} with root_dn") | ||||
|                 if self.root_dn is None: | ||||
|                     logger.error("root_dn missing in ldap config!") | ||||
|                     raise InternalServerError | ||||
|  | @ -141,14 +146,31 @@ class AuthLDAP(AuthPlugin): | |||
|                 ("display_name", "displayName"), | ||||
|             ]: | ||||
|                 if hasattr(user, name): | ||||
|                     modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])] | ||||
|                     attribute = getattr(user, name) | ||||
|                     if attribute: | ||||
|                         modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])] | ||||
|             if new_password: | ||||
|                 modifier["userPassword"] = [(MODIFY_REPLACE, [self.__hash(new_password)])] | ||||
|             if "userPassword" in modifier: | ||||
|                 logger.debug(f"LDAP: modify_user for user {user.userid} with password change (can't show >modifier<)") | ||||
|             else: | ||||
|                 logger.debug(f"LDAP: modify_user for user {user.userid} with modifier {modifier}") | ||||
|             ldap_conn.modify(dn, modifier) | ||||
|             self._set_roles(user) | ||||
|         except (LDAPPasswordIsMandatoryError, LDAPBindError): | ||||
|             raise BadRequest | ||||
| 
 | ||||
|     def get_modified_time(self, user): | ||||
|         self.ldap.connection.search( | ||||
|             self.search_dn, | ||||
|             "(uid={})".format(user.userid), | ||||
|             SUBTREE, | ||||
|             attributes=["modifyTimestamp"], | ||||
|         ) | ||||
|         r = self.ldap.connection.response[0]["attributes"] | ||||
|         modified_time = r["modifyTimestamp"][0] | ||||
|         return datetime.strptime(modified_time, "%Y%m%d%H%M%SZ") | ||||
| 
 | ||||
|     def get_avatar(self, user): | ||||
|         self.ldap.connection.search( | ||||
|             self.search_dn, | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ Extends users plugin with balance functions | |||
| 
 | ||||
| from flask import current_app | ||||
| from werkzeug.exceptions import NotFound | ||||
| from werkzeug.local import LocalProxy | ||||
| 
 | ||||
| from flaschengeist import logger | ||||
| from flaschengeist.config import config | ||||
|  | @ -82,3 +83,7 @@ class BalancePlugin(Plugin): | |||
|                 balance_controller.set_limit(user, limit, override=False) | ||||
|             except KeyError: | ||||
|                 pass | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def getPlugin() -> LocalProxy["BalancePlugin"]: | ||||
|         return LocalProxy(lambda: current_app.config["FG_PLUGINS"]["balance"]) | ||||
|  |  | |||
|  | @ -21,6 +21,8 @@ __attribute_limit = "balance_limit" | |||
| class NotifyType(IntEnum): | ||||
|     SEND_TO = 0x01 | ||||
|     SEND_FROM = 0x02 | ||||
|     ADD_FROM = 0x03 | ||||
|     SUB_FROM = 0x04 | ||||
| 
 | ||||
| 
 | ||||
| def set_limit(user: User, limit: float, override=True): | ||||
|  | @ -178,6 +180,7 @@ def send(sender: User, receiver, amount: float, author: User): | |||
|     Raises: | ||||
|         BadRequest if amount <= 0 | ||||
|     """ | ||||
|     logger.debug(f"send(sender={sender}, receiver={receiver}, amount={amount}, author={author})") | ||||
|     if amount <= 0: | ||||
|         raise BadRequest | ||||
| 
 | ||||
|  | @ -191,20 +194,40 @@ def send(sender: User, receiver, amount: float, author: User): | |||
|     db.session.add(transaction) | ||||
|     db.session.commit() | ||||
|     if sender is not None and sender.id_ != author.id_: | ||||
|         BalancePlugin.plugin.notify( | ||||
|             sender, | ||||
|             "Neue Transaktion", | ||||
|             { | ||||
|                 "type": NotifyType.SEND_FROM, | ||||
|                 "receiver_id": receiver.userid, | ||||
|                 "author_id": author.userid, | ||||
|                 "amount": amount, | ||||
|             }, | ||||
|         ) | ||||
|         if receiver is not None: | ||||
|             BalancePlugin.getPlugin().notify( | ||||
|                 sender, | ||||
|                 "Neue Transaktion", | ||||
|                 { | ||||
|                     "type": NotifyType.SEND_FROM, | ||||
|                     "receiver_id": receiver.userid, | ||||
|                     "author_id": author.userid, | ||||
|                     "amount": amount, | ||||
|                 }, | ||||
|             ) | ||||
|         else: | ||||
|             BalancePlugin.getPlugin().notify( | ||||
|                 sender, | ||||
|                 "Neue Transaktion", | ||||
|                 {"type": NotifyType.SUB_FROM, "author_id": author.userid, "amount": amount}, | ||||
|             ) | ||||
|     if receiver is not None and receiver.id_ != author.id_: | ||||
|         BalancePlugin.plugin.notify( | ||||
|             receiver, "Neue Transaktion", {"type": NotifyType.SEND_TO, "sender_id": sender.userid, "amount": amount} | ||||
|         ) | ||||
|         if sender is not None: | ||||
|             BalancePlugin.getPlugin().notify( | ||||
|                 receiver, | ||||
|                 "Neue Transaktion", | ||||
|                 { | ||||
|                     "type": NotifyType.SEND_TO, | ||||
|                     "sender_id": sender.userid, | ||||
|                     "amount": amount, | ||||
|                 }, | ||||
|             ) | ||||
|         else: | ||||
|             BalancePlugin.getPlugin().notify( | ||||
|                 receiver, | ||||
|                 "Neue Transaktion", | ||||
|                 {"type": NotifyType.ADD_FROM, "author_id": author.userid, "amount": amount}, | ||||
|             ) | ||||
|     return transaction | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ class MailMessagePlugin(Plugin): | |||
|             self.send_mail(msg) | ||||
| 
 | ||||
|     def send_mail(self, msg: Message): | ||||
|         logger.debug(f"Sending mail to {msg.receiver}") | ||||
|         logger.debug(f"Sending mail to {msg.receiver} with subject {msg.subject}") | ||||
|         if isinstance(msg.receiver, User): | ||||
|             if not msg.receiver.mail: | ||||
|                 logger.warning("Could not send Mail, mail missing: {}".format(msg.receiver)) | ||||
|  | @ -41,18 +41,12 @@ class MailMessagePlugin(Plugin): | |||
|             recipients = userController.get_user_by_role(msg.receiver) | ||||
| 
 | ||||
|         mail = MIMEMultipart() | ||||
|         try: | ||||
|             mail["From"] = self.mail | ||||
|             mail["To"] = ", ".join(recipients) | ||||
|         except Exception as e: | ||||
|             import traceback | ||||
| 
 | ||||
|             print(traceback.format_exc()) | ||||
|         mail["From"] = self.mail | ||||
|         mail["To"] = ", ".join(recipients) | ||||
|         mail["Subject"] = msg.subject | ||||
|         mail.attach(MIMEText(msg.message)) | ||||
|         if not hasattr(self, "smtp"): | ||||
|             self.__connect() | ||||
|         self.smtp.sendmail(self.mail, recipients, mail.as_string()) | ||||
|         with self.__connect() as smtp: | ||||
|             smtp.sendmail(self.mail, recipients, mail.as_string()) | ||||
| 
 | ||||
|     def __connect(self): | ||||
|         if self.crypt == "SSL": | ||||
|  | @ -63,3 +57,4 @@ class MailMessagePlugin(Plugin): | |||
|         else: | ||||
|             raise ValueError("Invalid CRYPT given") | ||||
|         self.smtp.login(self.user, self.password) | ||||
|         return self.smtp | ||||
|  |  | |||
|  | @ -3,8 +3,9 @@ | |||
| Provides routes used to manage users | ||||
| """ | ||||
| from http.client import CREATED | ||||
| from flask import Blueprint, request, jsonify, make_response | ||||
| from flask import Blueprint, request, jsonify, make_response, after_this_request, Response | ||||
| from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed | ||||
| from datetime import datetime | ||||
| 
 | ||||
| from . import permissions | ||||
| from flaschengeist import logger | ||||
|  | @ -118,10 +119,13 @@ def frontend(userid, current_session): | |||
| 
 | ||||
| 
 | ||||
| @UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["GET"]) | ||||
| @headers({"Cache-Control": "public, max-age=604800"}) | ||||
| @headers({"Cache-Control": "public, must-revalidate, max-age=10"}) | ||||
| def get_avatar(userid): | ||||
|     etag = None | ||||
|     if "If-None-Match" in request.headers: | ||||
|         etag = request.headers["If-None-Match"] | ||||
|     user = userController.get_user(userid) | ||||
|     return userController.load_avatar(user) | ||||
|     return userController.load_avatar(user, etag) | ||||
| 
 | ||||
| 
 | ||||
| @UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["POST"]) | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import sqlalchemy.exc | |||
| from flask.cli import with_appcontext | ||||
| from werkzeug.exceptions import NotFound | ||||
| 
 | ||||
| from flaschengeist import logger | ||||
| from flaschengeist.database import db | ||||
| from flaschengeist.controller import roleController, userController | ||||
| 
 | ||||
|  | @ -70,12 +71,19 @@ def user(add_role, delete, user): | |||
|         if USER_KEY in ctx.meta: | ||||
|             userController.register(ctx.meta[USER_KEY], ctx.meta[USER_KEY]["password"]) | ||||
|         else: | ||||
|             if not isinstance(user, list) or not isinstance(user, tuple): | ||||
|                 user = [user] | ||||
|             for uid in user: | ||||
|                 logger.debug(f"Userid: {uid}") | ||||
|                 user = userController.get_user(uid) | ||||
|                 logger.debug(f"User: {user}") | ||||
|                 if delete: | ||||
|                     logger.debug(f"Deleting user {user}") | ||||
|                     userController.delete_user(user) | ||||
|                 elif add_role: | ||||
|                     logger.debug(f"Adding role {add_role} to user {user}") | ||||
|                     role = roleController.get(add_role) | ||||
|                     logger.debug(f"Role: {role}") | ||||
|                     user.roles_.append(role) | ||||
|                     userController.modify_user(user, None) | ||||
|                     db.session.commit() | ||||
|  |  | |||
|  | @ -1,3 +1,6 @@ | |||
| [build-system] | ||||
| requires = ["setuptools", "wheel"] | ||||
| build-backend = "setuptools.build_meta" | ||||
| 
 | ||||
| [tool.black] | ||||
| line-length = 120 | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| [metadata] | ||||
| license = MIT | ||||
| version = 2.0.0.dev0 | ||||
| version = 2.0.0.dev1 | ||||
| name = flaschengeist | ||||
| author = Tim Gröger | ||||
| author_email = flaschengeist@wu5.de | ||||
|  | @ -45,7 +45,7 @@ mysql = | |||
|     mysqlclient;platform_system!='Windows' | ||||
| 
 | ||||
| [options.package_data] | ||||
| * = *.toml, script.py.mako | ||||
| * = *.toml, script.py.mako, *.ini, */migrations/*, migrations/versions/* | ||||
| 
 | ||||
| [options.entry_points] | ||||
| console_scripts = | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue