Compare commits

..

No commits in common. "b40d40644d8eab5f4f8eb678810a550565a7d69f" and "193ffeff9ded6235762da1b49fcf6713b4c74984" have entirely different histories.

14 changed files with 40 additions and 197 deletions

View File

@ -89,7 +89,6 @@ def main(*args, **kwargs):
from .docs_cmd import docs from .docs_cmd import docs
from .run_cmd import run from .run_cmd import run
from .install_cmd import install from .install_cmd import install
from .docker_cmd import docker
# Override logging level # Override logging level
environ.setdefault("FG_LOGGING", logging.getLevelName(LOGGING_MAX)) environ.setdefault("FG_LOGGING", logging.getLevelName(LOGGING_MAX))
@ -99,5 +98,4 @@ def main(*args, **kwargs):
cli.add_command(install) cli.add_command(install)
cli.add_command(plugin) cli.add_command(plugin)
cli.add_command(run) cli.add_command(run)
cli.add_command(docker)
cli(*args, **kwargs) cli(*args, **kwargs)

View File

@ -1,54 +0,0 @@
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")

View File

@ -108,12 +108,9 @@ def install_plugin(plugin_name: str):
plugin.install() plugin.install()
# Check migrations # Check migrations
directory = entry_point[0].dist.locate_file("") directory = entry_point[0].dist.locate_file("")
logger.debug(f"Checking for migrations in {directory}")
for loc in entry_point[0].module.split(".") + ["migrations"]: for loc in entry_point[0].module.split(".") + ["migrations"]:
directory /= loc directory /= loc
logger.debug(f"Checking for migrations with loc in {directory}")
if directory.exists(): if directory.exists():
logger.debug(f"Found migrations in {directory}")
database_upgrade(revision=f"{plugin_name}@head") database_upgrade(revision=f"{plugin_name}@head")
db.session.commit() db.session.commit()
return plugin return plugin

View File

@ -1,14 +1,11 @@
import re import re
import secrets import secrets
import hashlib
from io import BytesIO from io import BytesIO
from typing import Optional, Union from typing import Optional
from flask import make_response
from flask.json import provider
from sqlalchemy import exc from sqlalchemy import exc
from sqlalchemy_utils import merge_references from sqlalchemy_utils import merge_references
from datetime import datetime, timedelta, timezone, date from datetime import datetime, timedelta, timezone
from flask.helpers import send_file from flask.helpers import send_file
from werkzeug.exceptions import NotFound, BadRequest, Forbidden from werkzeug.exceptions import NotFound, BadRequest, Forbidden
@ -19,12 +16,7 @@ from ..models import Notification, User, Role
from ..models.user import _PasswordReset from ..models.user import _PasswordReset
from ..utils.hook import Hook from ..utils.hook import Hook
from ..utils.datetime import from_iso_format from ..utils.datetime import from_iso_format
from ..controller import ( from ..controller import imageController, messageController, pluginController, sessionController
imageController,
messageController,
pluginController,
sessionController,
)
from ..plugins import AuthPlugin from ..plugins import AuthPlugin
@ -203,11 +195,7 @@ def delete_user(user: User):
deleted_user = get_user("__deleted_user__", True) deleted_user = get_user("__deleted_user__", True)
except NotFound: except NotFound:
deleted_user = User( deleted_user = User(
userid="__deleted_user__", userid="__deleted_user__", firstname="USER", lastname="DELETED", display_name="DELETED USER", deleted=True
firstname="USER",
lastname="DELETED",
display_name="DELETED USER",
deleted=True,
) )
db.session.add(user) db.session.add(user)
db.session.flush() db.session.flush()
@ -218,10 +206,7 @@ def delete_user(user: User):
db.session.delete(user) db.session.delete(user)
db.session.commit() db.session.commit()
except exc.IntegrityError: except exc.IntegrityError:
logger.error( logger.error("Delete of user failed, there might be ForeignKey contraits from disabled plugins", exec_info=True)
"Delete of user failed, there might be ForeignKey contraits from disabled plugins",
exec_info=True,
)
# Remove at least all personal data # Remove at least all personal data
user.userid = f"__deleted_user__{user.id_}" user.userid = f"__deleted_user__{user.id_}"
user.display_name = "DELETED USER" user.display_name = "DELETED USER"
@ -243,9 +228,6 @@ def register(data, passwd=None):
values = {key: value for key, value in data.items() if key in allowed_keys} values = {key: value for key, value in data.items() if key in allowed_keys}
roles = values.pop("roles", []) roles = values.pop("roles", [])
if "birthday" in data: if "birthday" in data:
if isinstance(data["birthday"], date):
values["birthday"] = data["birthday"]
else:
values["birthday"] = from_iso_format(data["birthday"]).date() values["birthday"] = from_iso_format(data["birthday"]).date()
if "mail" in data and not re.match(r"[^@]+@[^@]+\.[^@]+", data["mail"]): if "mail" in data and not re.match(r"[^@]+@[^@]+\.[^@]+", data["mail"]):
raise BadRequest("Invalid mail given") raise BadRequest("Invalid mail given")
@ -280,22 +262,14 @@ def register(data, passwd=None):
return user return user
def get_last_modified(user: User): def load_avatar(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: if user.avatar_ is not None:
return imageController.send_image(image=user.avatar_) return imageController.send_image(image=user.avatar_)
else: else:
provider = get_provider(user.userid) provider = get_provider(user.userid)
avatar = provider.get_avatar(user) 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: if len(avatar.binary) > 0:
return send_file(BytesIO(avatar.binary), avatar.mimetype, etag=new_etag) return send_file(BytesIO(avatar.binary), avatar.mimetype)
raise NotFound raise NotFound

View File

@ -1,6 +1,4 @@
from __future__ import ( from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
annotations,
) # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from typing import Optional, Union, List from typing import Optional, Union, List
from datetime import date, datetime from datetime import date, datetime
@ -47,7 +45,7 @@ class User(db.Model, ModelSerializeMixin):
Attributes: Attributes:
id: Id in Database as Primary Key. id: Id in Database as Primary Key.
userid: User ID used by authentication provider uid: User ID used by authentication provider
display_name: Name to show display_name: Name to show
firstname: Firstname of the User firstname: Firstname of the User
lastname: Lastname of the User lastname: Lastname of the User

View File

@ -248,16 +248,6 @@ class AuthPlugin(Plugin):
""" """
raise NotImplementedError 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: def get_avatar(self, user) -> _Avatar:
"""Retrieve avatar for given user (if supported by auth backend) """Retrieve avatar for given user (if supported by auth backend)

View File

@ -10,8 +10,6 @@ from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from datetime import datetime
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.config import config from flaschengeist.config import config
from flaschengeist.controller import userController from flaschengeist.controller import userController
@ -128,12 +126,9 @@ class AuthLDAP(AuthPlugin):
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")
logger.debug(f"LDAP: modify_user for user {user.userid} with dn {dn}")
if password: if password:
logger.debug(f"LDAP: modify_user for user {user.userid} with password")
ldap_conn = self.ldap.connect(dn, password) ldap_conn = self.ldap.connect(dn, password)
else: else:
logger.debug(f"LDAP: modify_user for user {user.userid} with root_dn")
if self.root_dn is None: if self.root_dn is None:
logger.error("root_dn missing in ldap config!") logger.error("root_dn missing in ldap config!")
raise InternalServerError raise InternalServerError
@ -146,31 +141,14 @@ class AuthLDAP(AuthPlugin):
("display_name", "displayName"), ("display_name", "displayName"),
]: ]:
if hasattr(user, name): if hasattr(user, name):
attribute = getattr(user, name)
if attribute:
modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])] modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])]
if new_password: if new_password:
modifier["userPassword"] = [(MODIFY_REPLACE, [self.__hash(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) ldap_conn.modify(dn, modifier)
self._set_roles(user) self._set_roles(user)
except (LDAPPasswordIsMandatoryError, LDAPBindError): except (LDAPPasswordIsMandatoryError, LDAPBindError):
raise BadRequest 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): def get_avatar(self, user):
self.ldap.connection.search( self.ldap.connection.search(
self.search_dn, self.search_dn,

View File

@ -5,7 +5,6 @@ Extends users plugin with balance functions
from flask import current_app from flask import current_app
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from werkzeug.local import LocalProxy
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.config import config from flaschengeist.config import config
@ -83,7 +82,3 @@ class BalancePlugin(Plugin):
balance_controller.set_limit(user, limit, override=False) balance_controller.set_limit(user, limit, override=False)
except KeyError: except KeyError:
pass pass
@staticmethod
def getPlugin() -> LocalProxy["BalancePlugin"]:
return LocalProxy(lambda: current_app.config["FG_PLUGINS"]["balance"])

View File

@ -21,8 +21,6 @@ __attribute_limit = "balance_limit"
class NotifyType(IntEnum): class NotifyType(IntEnum):
SEND_TO = 0x01 SEND_TO = 0x01
SEND_FROM = 0x02 SEND_FROM = 0x02
ADD_FROM = 0x03
SUB_FROM = 0x04
def set_limit(user: User, limit: float, override=True): def set_limit(user: User, limit: float, override=True):
@ -180,7 +178,6 @@ def send(sender: User, receiver, amount: float, author: User):
Raises: Raises:
BadRequest if amount <= 0 BadRequest if amount <= 0
""" """
logger.debug(f"send(sender={sender}, receiver={receiver}, amount={amount}, author={author})")
if amount <= 0: if amount <= 0:
raise BadRequest raise BadRequest
@ -194,8 +191,7 @@ def send(sender: User, receiver, amount: float, author: User):
db.session.add(transaction) db.session.add(transaction)
db.session.commit() db.session.commit()
if sender is not None and sender.id_ != author.id_: if sender is not None and sender.id_ != author.id_:
if receiver is not None: BalancePlugin.plugin.notify(
BalancePlugin.getPlugin().notify(
sender, sender,
"Neue Transaktion", "Neue Transaktion",
{ {
@ -205,28 +201,9 @@ def send(sender: User, receiver, amount: float, author: User):
"amount": amount, "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_: if receiver is not None and receiver.id_ != author.id_:
if sender is not None: BalancePlugin.plugin.notify(
BalancePlugin.getPlugin().notify( receiver, "Neue Transaktion", {"type": NotifyType.SEND_TO, "sender_id": sender.userid, "amount": amount}
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 return transaction

View File

@ -31,7 +31,7 @@ class MailMessagePlugin(Plugin):
self.send_mail(msg) self.send_mail(msg)
def send_mail(self, msg: Message): def send_mail(self, msg: Message):
logger.debug(f"Sending mail to {msg.receiver} with subject {msg.subject}") logger.debug(f"Sending mail to {msg.receiver}")
if isinstance(msg.receiver, User): if isinstance(msg.receiver, User):
if not msg.receiver.mail: if not msg.receiver.mail:
logger.warning("Could not send Mail, mail missing: {}".format(msg.receiver)) logger.warning("Could not send Mail, mail missing: {}".format(msg.receiver))
@ -41,12 +41,18 @@ class MailMessagePlugin(Plugin):
recipients = userController.get_user_by_role(msg.receiver) recipients = userController.get_user_by_role(msg.receiver)
mail = MIMEMultipart() mail = MIMEMultipart()
try:
mail["From"] = self.mail mail["From"] = self.mail
mail["To"] = ", ".join(recipients) mail["To"] = ", ".join(recipients)
except Exception as e:
import traceback
print(traceback.format_exc())
mail["Subject"] = msg.subject mail["Subject"] = msg.subject
mail.attach(MIMEText(msg.message)) mail.attach(MIMEText(msg.message))
with self.__connect() as smtp: if not hasattr(self, "smtp"):
smtp.sendmail(self.mail, recipients, mail.as_string()) self.__connect()
self.smtp.sendmail(self.mail, recipients, mail.as_string())
def __connect(self): def __connect(self):
if self.crypt == "SSL": if self.crypt == "SSL":
@ -57,4 +63,3 @@ class MailMessagePlugin(Plugin):
else: else:
raise ValueError("Invalid CRYPT given") raise ValueError("Invalid CRYPT given")
self.smtp.login(self.user, self.password) self.smtp.login(self.user, self.password)
return self.smtp

View File

@ -3,9 +3,8 @@
Provides routes used to manage users Provides routes used to manage users
""" """
from http.client import CREATED from http.client import CREATED
from flask import Blueprint, request, jsonify, make_response, after_this_request, Response from flask import Blueprint, request, jsonify, make_response
from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed
from datetime import datetime
from . import permissions from . import permissions
from flaschengeist import logger from flaschengeist import logger
@ -119,13 +118,10 @@ def frontend(userid, current_session):
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["GET"]) @UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["GET"])
@headers({"Cache-Control": "public, must-revalidate, max-age=10"}) @headers({"Cache-Control": "public, max-age=604800"})
def get_avatar(userid): def get_avatar(userid):
etag = None
if "If-None-Match" in request.headers:
etag = request.headers["If-None-Match"]
user = userController.get_user(userid) user = userController.get_user(userid)
return userController.load_avatar(user, etag) return userController.load_avatar(user)
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["POST"]) @UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["POST"])

View File

@ -3,7 +3,6 @@ import sqlalchemy.exc
from flask.cli import with_appcontext from flask.cli import with_appcontext
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from flaschengeist import logger
from flaschengeist.database import db from flaschengeist.database import db
from flaschengeist.controller import roleController, userController from flaschengeist.controller import roleController, userController
@ -71,19 +70,12 @@ def user(add_role, delete, user):
if USER_KEY in ctx.meta: if USER_KEY in ctx.meta:
userController.register(ctx.meta[USER_KEY], ctx.meta[USER_KEY]["password"]) userController.register(ctx.meta[USER_KEY], ctx.meta[USER_KEY]["password"])
else: else:
if not isinstance(user, list) or not isinstance(user, tuple):
user = [user]
for uid in user: for uid in user:
logger.debug(f"Userid: {uid}")
user = userController.get_user(uid) user = userController.get_user(uid)
logger.debug(f"User: {user}")
if delete: if delete:
logger.debug(f"Deleting user {user}")
userController.delete_user(user) userController.delete_user(user)
elif add_role: elif add_role:
logger.debug(f"Adding role {add_role} to user {user}")
role = roleController.get(add_role) role = roleController.get(add_role)
logger.debug(f"Role: {role}")
user.roles_.append(role) user.roles_.append(role)
userController.modify_user(user, None) userController.modify_user(user, None)
db.session.commit() db.session.commit()

View File

@ -1,6 +1,3 @@
[build-system] [build-system]
requires = ["setuptools", "wheel"] requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.black]
line-length = 120

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
license = MIT license = MIT
version = 2.0.0.dev1 version = 2.0.0.dev0
name = flaschengeist name = flaschengeist
author = Tim Gröger author = Tim Gröger
author_email = flaschengeist@wu5.de author_email = flaschengeist@wu5.de
@ -45,7 +45,7 @@ mysql =
mysqlclient;platform_system!='Windows' mysqlclient;platform_system!='Windows'
[options.package_data] [options.package_data]
* = *.toml, script.py.mako, *.ini, */migrations/*, migrations/versions/* * = *.toml, script.py.mako
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =