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 .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))
@ -99,5 +98,4 @@ def main(*args, **kwargs):
cli.add_command(install)
cli.add_command(plugin)
cli.add_command(run)
cli.add_command(docker)
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()
# 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

View File

@ -1,14 +1,11 @@
import re
import secrets
import hashlib
from io import BytesIO
from typing import Optional, Union
from flask import make_response
from flask.json import provider
from typing import Optional
from sqlalchemy import exc
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 werkzeug.exceptions import NotFound, BadRequest, Forbidden
@ -19,12 +16,7 @@ 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
@ -203,11 +195,7 @@ 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()
@ -218,10 +206,7 @@ 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"
@ -243,10 +228,7 @@ 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:
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"]):
raise BadRequest("Invalid mail given")
user = User(**values)
@ -280,22 +262,14 @@ def register(data, passwd=None):
return 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):
def load_avatar(user: User):
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, etag=new_etag)
return send_file(BytesIO(avatar.binary), avatar.mimetype)
raise NotFound

View File

@ -1,6 +1,4 @@
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
@ -47,7 +45,7 @@ class User(db.Model, ModelSerializeMixin):
Attributes:
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
firstname: Firstname of the User
lastname: Lastname of the User

View File

@ -248,16 +248,6 @@ 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)

View File

@ -10,8 +10,6 @@ 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
@ -128,12 +126,9 @@ 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
@ -146,31 +141,14 @@ class AuthLDAP(AuthPlugin):
("display_name", "displayName"),
]:
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:
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,

View File

@ -5,7 +5,6 @@ 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
@ -83,7 +82,3 @@ 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"])

View File

@ -21,8 +21,6 @@ __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):
@ -180,7 +178,6 @@ 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
@ -194,40 +191,20 @@ 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_:
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},
)
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 and receiver.id_ != author.id_:
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},
)
BalancePlugin.plugin.notify(
receiver, "Neue Transaktion", {"type": NotifyType.SEND_TO, "sender_id": sender.userid, "amount": amount}
)
return transaction

View File

@ -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} with subject {msg.subject}")
logger.debug(f"Sending mail to {msg.receiver}")
if isinstance(msg.receiver, User):
if not msg.receiver.mail:
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)
mail = MIMEMultipart()
mail["From"] = self.mail
mail["To"] = ", ".join(recipients)
try:
mail["From"] = self.mail
mail["To"] = ", ".join(recipients)
except Exception as e:
import traceback
print(traceback.format_exc())
mail["Subject"] = msg.subject
mail.attach(MIMEText(msg.message))
with self.__connect() as smtp:
smtp.sendmail(self.mail, recipients, mail.as_string())
if not hasattr(self, "smtp"):
self.__connect()
self.smtp.sendmail(self.mail, recipients, mail.as_string())
def __connect(self):
if self.crypt == "SSL":
@ -57,4 +63,3 @@ class MailMessagePlugin(Plugin):
else:
raise ValueError("Invalid CRYPT given")
self.smtp.login(self.user, self.password)
return self.smtp

View File

@ -3,9 +3,8 @@
Provides routes used to manage users
"""
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 datetime import datetime
from . import permissions
from flaschengeist import logger
@ -119,13 +118,10 @@ def frontend(userid, current_session):
@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):
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, etag)
return userController.load_avatar(user)
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["POST"])

View File

@ -3,7 +3,6 @@ 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
@ -71,19 +70,12 @@ 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()

View File

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

View File

@ -1,6 +1,6 @@
[metadata]
license = MIT
version = 2.0.0.dev1
version = 2.0.0.dev0
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, *.ini, */migrations/*, migrations/versions/*
* = *.toml, script.py.mako
[options.entry_points]
console_scripts =