Merge branch 'develop' into feature/pricelist

This commit is contained in:
Tim Gröger 2021-11-11 19:49:43 +01:00
commit ff13eefb45
13 changed files with 209 additions and 109 deletions

View File

@ -54,6 +54,8 @@ def __load_plugins(app):
logger.error( logger.error(
f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.", exc_info=True 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): if isinstance(plugin, AuthPlugin):
logger.debug(f"Found authentication plugin: {entry_point.name}") logger.debug(f"Found authentication plugin: {entry_point.name}")
if entry_point.name == config["FLASCHENGEIST"]["auth"]: if entry_point.name == config["FLASCHENGEIST"]["auth"]:
@ -65,6 +67,7 @@ def __load_plugins(app):
app.config["FG_PLUGINS"][entry_point.name] = plugin app.config["FG_PLUGINS"][entry_point.name] = plugin
if "FG_AUTH_BACKEND" not in app.config: if "FG_AUTH_BACKEND" not in app.config:
logger.error("No authentication plugin configured or authentication plugin not found") 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(): def install_all():

View File

@ -2,7 +2,7 @@ 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, case_sensitive
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.utils.hook import Hook from flaschengeist.utils.hook import Hook
@ -36,8 +36,8 @@ def update_role(role, new_name):
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")
elif role.name != new_name: else:
if db.session.query(db.exists().where(Role.name == new_name)).scalar(): if role.name == new_name or db.session.query(db.exists().where(Role.name == case_sensitive(new_name))).scalar():
raise BadRequest("Name already used") raise BadRequest("Name already used")
role.name = new_name role.name = new_name
db.session.commit() db.session.commit()

View File

@ -1,3 +1,10 @@
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() db = SQLAlchemy()
def case_sensitive(s):
if db.session.bind.dialect.name == "mysql":
from sqlalchemy import func
return func.binary(s)
return s

View File

@ -29,17 +29,16 @@ level = "WARNING"
[auth_plain] [auth_plain]
enabled = true enabled = true
#[auth_ldap] [auth_ldap]
# enabled = true # Full documentation https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/plugins_auth_ldap
# host = enabled = false
# port = # host = "localhost"
# bind_dn = # port = 389
# base_dn = # base_dn = "dc=example,dc=com"
# secret = # root_dn = "cn=Manager,dc=example,dc=com"
# use_ssl = # root_secret = "SuperS3cret"
# admin_dn = # Uncomment to use secured LDAP (ldaps)
# admin_dn = # use_ssl = true
# default_gid =
[MESSAGES] [MESSAGES]
welcome_subject = "Welcome to Flaschengeist {name}" welcome_subject = "Welcome to Flaschengeist {name}"

View File

@ -56,7 +56,7 @@ class User(db.Model, ModelSerializeMixin):
display_name: str = db.Column(db.String(30)) display_name: str = db.Column(db.String(30))
firstname: str = db.Column(db.String(50), nullable=False) firstname: str = db.Column(db.String(50), nullable=False)
lastname: 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) birthday: Optional[date] = db.Column(db.Date)
roles: list[str] = [] roles: list[str] = []
permissions: Optional[list[str]] = None permissions: Optional[list[str]] = None

View File

@ -33,6 +33,7 @@ class Plugin:
blueprint = None # You have to override blueprint = None # You have to override
permissions = [] # You have to override permissions = [] # You have to override
id = "dev.flaschengeist.plugin" # You have to override
name = "plugin" # You have to override name = "plugin" # You have to override
models = None # You have to override models = None # You have to override
@ -94,7 +95,7 @@ class Plugin:
db.session.commit() db.session.commit()
def notify(self, user, text: str, data=None): 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.add(n)
db.session.commit() db.session.commit()

View File

@ -1,56 +1,61 @@
"""LDAP Authentication Provider Plugin""" """LDAP Authentication Provider Plugin"""
import io import io
import os
import ssl import ssl
from typing import Optional from typing import Optional
from flask_ldapconn import LDAPConn from flask_ldapconn import LDAPConn
from flask import current_app as app from flask import current_app as app
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
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
from flaschengeist import logger 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 from flaschengeist.models.user import User, Role, _Avatar
import flaschengeist.controller.userController as userController import flaschengeist.controller.userController as userController
class AuthLDAP(AuthPlugin): class AuthLDAP(AuthPlugin):
def __init__(self, cfg): def __init__(self, config):
super().__init__() super().__init__()
config = {"port": 389, "use_ssl": False}
config.update(cfg)
app.config.update( app.config.update(
LDAP_SERVER=config["host"], LDAP_SERVER=config.get("host", "localhost"),
LDAP_PORT=config["port"], LDAP_PORT=config.get("port", 389),
LDAP_BINDDN=config["bind_dn"], 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_TLS=False,
LDAP_USE_SSL=config["use_ssl"], LDAP_TLS_VERSION=ssl.PROTOCOL_TLS,
LDAP_TLS_VERSION=ssl.PROTOCOL_TLSv1_2,
LDAP_REQUIRE_CERT=ssl.CERT_NONE,
FORCE_ATTRIBUTE_VALUE_AS_LIST=True, FORCE_ATTRIBUTE_VALUE_AS_LIST=True,
) )
if "secret" in config: logger.warning(app.config.get("LDAP_USE_SSL"))
app.config["LDAP_SECRET"] = config["secret"] 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.ldap = LDAPConn(app)
self.dn = config["base_dn"] self.base_dn = config["base_dn"]
self.default_gid = config["default_gid"] 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 # TODO: might not be set if modify is called
if "admin_dn" in config: self.root_dn = config.get("root_dn", None)
self.admin_dn = config["admin_dn"] self.root_secret = config.get("root_secret", None)
self.admin_secret = config["admin_secret"]
else:
self.admin_dn = None
@after_role_updated @before_role_updated
def _role_updated(role, new_name): def _role_updated(role, new_name):
logger.debug(f"LDAP: before_role_updated called with ({role}, {new_name})")
self.__modify_role(role, new_name) self.__modify_role(role, new_name)
def login(self, user, password): def login(self, user, password):
if not user: if not user:
return False 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): def find_user(self, userid, mail=None):
attr = self.__find(userid, mail) attr = self.__find(userid, mail)
@ -64,42 +69,41 @@ class AuthLDAP(AuthPlugin):
self.__update(user, attr) self.__update(user, attr)
def create_user(self, user, password): def create_user(self, user, password):
if self.admin_dn is None: if self.root_dn is None:
logger.error("admin_dn missing in ldap config!") logger.error("root_dn missing in ldap config!")
raise InternalServerError raise InternalServerError
try: try:
ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) 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.ldap.connection.search(
"ou=user,{}".format(self.dn), self.search_dn,
"(uidNumber=*)", "(uidNumber=*)",
SUBTREE, SUBTREE,
attributes=["uidNumber"], attributes=["uidNumber"],
) )
uid_number = ( resp = sorted(
sorted(self.ldap.response(), key=lambda i: i["attributes"]["uidNumber"], reverse=True,)[0][ self.ldap.response(),
"attributes" key=lambda i: i["attributes"]["uidNumber"],
]["uidNumber"] reverse=True,
+ 1
) )
dn = f"cn={user.firstname} {user.lastname},ou=user,{self.dn}" attributes = resp[0]["attributes"]["uidNumber"] + 1 if resp else attributes["uidNumber"]
object_class = [ dn = self.dn_template.format(
"inetOrgPerson", firstname=user.firstname,
"posixAccount", lastname=user.lastname,
"person", userid=user.userid,
"organizationalPerson", mail=user.mail,
] display_name=user.display_name,
attributes = { base_dn=self.base_dn,
"sn": user.firstname, )
"givenName": user.lastname, attributes.update({
"gidNumber": self.default_gid, "sn": user.lastname,
"homeDirectory": f"/home/{user.userid}", "givenName": user.firstname,
"loginShell": "/bin/bash",
"uid": user.userid, "uid": user.userid,
"userPassword": hashed(HASHED_SALTED_MD5, password), "userPassword": self.__hash(password),
"uidNumber": uid_number, })
} ldap_conn.add(dn, self.object_classes, 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
@ -110,10 +114,10 @@ class AuthLDAP(AuthPlugin):
if password: if password:
ldap_conn = self.ldap.connect(dn, password) ldap_conn = self.ldap.connect(dn, password)
else: else:
if self.admin_dn is None: if self.root_dn is None:
logger.error("admin_dn missing in ldap config!") logger.error("root_dn missing in ldap config!")
raise InternalServerError 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 = {} modifier = {}
for name, ldap_name in [ for name, ldap_name in [
("firstname", "givenName"), ("firstname", "givenName"),
@ -124,9 +128,7 @@ class AuthLDAP(AuthPlugin):
if hasattr(user, name): if hasattr(user, name):
modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])] modifier[ldap_name] = [(MODIFY_REPLACE, [getattr(user, name)])]
if new_password: if new_password:
# TODO: Use secure hash! modifier["userPassword"] = [(MODIFY_REPLACE, [self.__hash(new_password)])]
salted_password = hashed(HASHED_SALTED_MD5, new_password)
modifier["userPassword"] = [(MODIFY_REPLACE, [salted_password])]
ldap_conn.modify(dn, modifier) ldap_conn.modify(dn, modifier)
self._set_roles(user) self._set_roles(user)
except (LDAPPasswordIsMandatoryError, LDAPBindError): except (LDAPPasswordIsMandatoryError, LDAPBindError):
@ -134,7 +136,7 @@ class AuthLDAP(AuthPlugin):
def get_avatar(self, user): def get_avatar(self, user):
self.ldap.connection.search( self.ldap.connection.search(
"ou=user,{}".format(self.dn), self.search_dn,
"(uid={})".format(user.userid), "(uid={})".format(user.userid),
SUBTREE, SUBTREE,
attributes=["jpegPhoto"], attributes=["jpegPhoto"],
@ -150,8 +152,8 @@ class AuthLDAP(AuthPlugin):
raise NotFound raise NotFound
def set_avatar(self, user, avatar: _Avatar): def set_avatar(self, user, avatar: _Avatar):
if self.admin_dn is None: if self.root_dn is None:
logger.error("admin_dn missing in ldap config!") logger.error("root_dn missing in ldap config!")
raise InternalServerError raise InternalServerError
if avatar.mimetype != "image/jpeg": if avatar.mimetype != "image/jpeg":
@ -172,16 +174,16 @@ class AuthLDAP(AuthPlugin):
raise BadRequest("Unsupported image format") raise BadRequest("Unsupported image format")
dn = user.get_attribute("DN") 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])]}) ldap_conn.modify(dn, {"jpegPhoto": [(MODIFY_REPLACE, [avatar.binary])]})
def __find(self, userid, mail=None): def __find(self, userid, mail=None):
"""Find attributes of an user by uid or mail in LDAP""" """Find attributes of an user by uid or mail in LDAP"""
con = self.ldap.connection con = self.ldap.connection
if not con: 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( con.search(
f"ou=user,{self.dn}", self.search_dn,
f"(| (uid={userid})(mail={mail}))" if mail else f"(uid={userid})", f"(| (uid={userid})(mail={mail}))" if mail else f"(uid={userid})",
SUBTREE, SUBTREE,
attributes=["uid", "givenName", "sn", "mail"], attributes=["uid", "givenName", "sn", "mail"],
@ -205,12 +207,12 @@ class AuthLDAP(AuthPlugin):
role: Role, role: Role,
new_name: Optional[str], new_name: Optional[str],
): ):
if self.admin_dn is None: if self.root_dn is None:
logger.error("admin_dn missing in ldap config!") logger.error("root_dn missing in ldap config!")
raise InternalServerError raise InternalServerError
try: try:
ldap_conn = self.ldap.connect(self.admin_dn, self.admin_secret) ldap_conn = self.ldap.connect(self.root_dn, self.root_secret)
ldap_conn.search(f"ou=group,{self.dn}", f"(cn={role.name})", SUBTREE, attributes=["cn"]) ldap_conn.search(self.group_dn, f"(cn={role.name})", SUBTREE, attributes=["cn"])
if len(ldap_conn.response) > 0: if len(ldap_conn.response) > 0:
dn = ldap_conn.response[0]["dn"] dn = ldap_conn.response[0]["dn"]
if new_name: if new_name:
@ -218,13 +220,33 @@ class AuthLDAP(AuthPlugin):
else: else:
ldap_conn.delete(dn) ldap_conn.delete(dn)
except (LDAPPasswordIsMandatoryError, LDAPBindError): except LDAPPasswordIsMandatoryError:
raise BadRequest 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 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): def _get_groups(self, uid):
groups = [] groups = []
self.ldap.connection.search( self.ldap.connection.search(
"ou=group,{}".format(self.dn), self.group_dn,
"(memberUID={})".format(uid), "(memberUID={})".format(uid),
SUBTREE, SUBTREE,
attributes=["cn"], attributes=["cn"],
@ -236,7 +258,7 @@ class AuthLDAP(AuthPlugin):
def _get_all_roles(self): def _get_all_roles(self):
self.ldap.connection.search( self.ldap.connection.search(
f"ou=group,{self.dn}", self.group_dn,
"(cn=*)", "(cn=*)",
SUBTREE, SUBTREE,
attributes=["cn", "gidNumber", "memberUid"], attributes=["cn", "gidNumber", "memberUid"],
@ -245,8 +267,7 @@ class AuthLDAP(AuthPlugin):
def _set_roles(self, user: User): def _set_roles(self, user: User):
try: 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() 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)
@ -255,7 +276,7 @@ class AuthLDAP(AuthPlugin):
for user_role in user.roles: for user_role in user.roles:
if user_role not in [role["attributes"]["cn"][0] for role in ldap_roles]: if user_role not in [role["attributes"]["cn"][0] for role in ldap_roles]:
ldap_conn.add( ldap_conn.add(
f"cn={user_role},ou=group,{self.dn}", f"cn={user_role},{self.group_dn}",
["posixGroup"], ["posixGroup"],
attributes={"gidNumber": gid_number}, attributes={"gidNumber": gid_number},
) )

View File

@ -113,7 +113,9 @@ def get_transaction(transaction_id) -> Transaction:
return 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 count = None
query = Transaction.query.filter((Transaction.sender_ == user) | (Transaction.receiver_ == user)) query = Transaction.query.filter((Transaction.sender_ == user) | (Transaction.receiver_ == user))
if start: if start:
@ -125,6 +127,9 @@ def get_transactions(user, start=None, end=None, limit=None, offset=None, show_r
query = query.filter(Transaction.original_ == None) query = query.filter(Transaction.original_ == None)
if not show_cancelled: if not show_cancelled:
query = query.filter(Transaction.reversal_id.is_(None)) query = query.filter(Transaction.reversal_id.is_(None))
if descending:
query = query.order_by(Transaction.time.desc())
else:
query = query.order_by(Transaction.time) query = query.order_by(Transaction.time)
if limit is not None: if limit is not None:
count = query.count() count = query.count()

View File

@ -99,6 +99,32 @@ def set_limit(userid, current_session: Session):
return HTTP.no_content() 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/<userid>/balance", methods=["GET"]) @BalancePlugin.blueprint.route("/users/<userid>/balance", methods=["GET"])
@login_required(permission=permissions.SHOW) @login_required(permission=permissions.SHOW)
def get_balance(userid, current_session: Session): def get_balance(userid, current_session: Session):
@ -170,6 +196,7 @@ def get_transactions(userid, current_session: Session):
show_cancelled = request.args.get("showCancelled", True) show_cancelled = request.args.get("showCancelled", True)
limit = request.args.get("limit") limit = request.args.get("limit")
offset = request.args.get("offset") offset = request.args.get("offset")
descending = request.args.get("descending", False)
try: try:
if limit is not None: if limit is not None:
limit = int(limit) limit = int(limit)
@ -179,11 +206,20 @@ def get_transactions(userid, current_session: Session):
show_reversals = str2bool(show_reversals) show_reversals = str2bool(show_reversals)
if not isinstance(show_cancelled, bool): if not isinstance(show_cancelled, bool):
show_cancelled = str2bool(show_cancelled) show_cancelled = str2bool(show_cancelled)
if not isinstance(descending, bool):
descending = str2bool(descending)
except ValueError: except ValueError:
raise BadRequest raise BadRequest
transactions, count = balance_controller.get_transactions( 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} return {"transactions": transactions, "count": count}

View File

@ -11,6 +11,7 @@ from . import permissions, models
class EventPlugin(Plugin): class EventPlugin(Plugin):
name = "events" name = "events"
id = "dev.flaschengeist.events"
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.name]) plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.name])
permissions = permissions.permissions permissions = permissions.permissions
blueprint = Blueprint(name, __name__) blueprint = Blueprint(name, __name__)

View File

@ -116,7 +116,7 @@ def get_event(event_id, with_backup=False) -> Event:
if event is None: if event is None:
raise NotFound raise NotFound
if not with_backup: if not with_backup:
return clear_backup(event) clear_backup(event)
return event return event
@ -153,7 +153,9 @@ def delete_event(event_id):
Raises: Raises:
NotFound if not found NotFound if not found
""" """
event = get_event(event_id) event = get_event(event_id, True)
for job in event.jobs:
delete_job(job)
db.session.delete(event) db.session.delete(event)
db.session.commit() db.session.commit()
@ -201,17 +203,15 @@ def update():
def delete_job(job: Job): def delete_job(job: Job):
for service in job.services:
unassign_job(service=service, notify=True)
db.session.delete(job) db.session.delete(job)
db.session.commit() 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_)) service = Service.query.get((job.id, user.id_))
if value < 0:
if not service:
raise BadRequest
db.session.delete(service)
else:
if service: if service:
service.value = value service.value = value
else: else:
@ -220,6 +220,25 @@ def assign_to_job(job: Job, user, value):
db.session.commit() 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 @scheduled
def assign_backups(): def assign_backups():
logger.debug("Notifications") logger.debug("Notifications")

View File

@ -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) user != current_session.user_ and not current_session.user_.has_permission(permissions.ASSIGN_OTHER)
): ):
raise Forbidden 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): except (KeyError, ValueError):
raise BadRequest raise BadRequest

View File

@ -41,7 +41,12 @@ setup(
"werkzeug", "werkzeug",
mysql_driver, 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={ entry_points={
"flaschengeist.plugin": [ "flaschengeist.plugin": [
# Authentication providers # Authentication providers