From 2c55edf6a8f1fd7022f4f6eab64bf94d902d7469 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 15 Oct 2020 21:58:56 +0200 Subject: [PATCH] Only use one plugin system, load auth and "normal" plugins at once. * Added Plugin class, where to inheritate from --- flaschengeist/app.py | 33 +- flaschengeist/flaschengeist.example.cfg | 20 +- flaschengeist/modules/__init__.py | 12 +- flaschengeist/modules/auth/__init__.py | 151 +++++----- flaschengeist/modules/auth_ldap/__init__.py | 31 +- flaschengeist/modules/message_mail.py | 43 +++ flaschengeist/modules/roles/__init__.py | 112 ++++--- flaschengeist/modules/schedule/__init__.py | 283 +++++++++--------- flaschengeist/modules/users/__init__.py | 83 +++-- .../system/controller/emailController.py | 119 -------- .../system/controller/eventController.py | 3 +- .../system/controller/messageController.py | 14 + .../system/controller/userController.py | 6 +- setup.py | 15 +- 14 files changed, 428 insertions(+), 497 deletions(-) create mode 100644 flaschengeist/modules/message_mail.py delete mode 100644 flaschengeist/system/controller/emailController.py create mode 100644 flaschengeist/system/controller/messageController.py diff --git a/flaschengeist/app.py b/flaschengeist/app.py index c96c817..968e46e 100644 --- a/flaschengeist/app.py +++ b/flaschengeist/app.py @@ -6,6 +6,7 @@ from flask.json import JSONEncoder, jsonify from werkzeug.exceptions import HTTPException from . import logger +from .modules import AuthPlugin from .system.config import config, configure_app from .system.controller import roleController @@ -30,19 +31,6 @@ class CustomJSONEncoder(JSONEncoder): return JSONEncoder.default(self, o) -def __load_auth(app): - for entry_point in pkg_resources.iter_entry_points('flaschengeist.auth'): - logger.debug('Found authentication plugin: %s', entry_point.name) - if entry_point.name == config['FLASCHENGEIST']['AUTH']: - app.config['FG_AUTH_BACKEND'] = entry_point.load()() - app.config['FG_AUTH_BACKEND'].configure( - config[entry_point.name] if config.has_section(entry_point.name) else {}) - logger.info('Loaded authentication plugin > %s <', entry_point.name) - break - if not app.config['FG_AUTH_BACKEND']: - logger.error('No authentication plugin configured or authentication plugin not found') - - def __load_plugins(app): logger.info('Search for plugins') app.config['FG_PLUGINS'] = {} @@ -50,10 +38,20 @@ def __load_plugins(app): logger.debug("Found plugin: >{}<".format(entry_point.name)) plugin = None if config.get(entry_point.name, 'enabled', fallback=False): - plugin = entry_point.load()() - app.register_blueprint(plugin.blueprint) - logger.info("Loaded plugin >{}<".format(entry_point.name)) - app.config["FG_PLUGINS"][entry_point.name] = plugin + plugin = entry_point.load()(config[entry_point.name] if config.has_section(entry_point.name) else {}) + if plugin.blueprint: + app.register_blueprint(plugin.blueprint) + logger.info("Load plugin >{}<".format(entry_point.name)) + if isinstance(plugin, AuthPlugin): + logger.debug('Found authentication plugin: %s', entry_point.name) + if entry_point.name == config['FLASCHENGEIST']['AUTH']: + app.config['FG_AUTH_BACKEND'] = plugin + else: + del plugin + else: + 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') def install_all(): @@ -77,7 +75,6 @@ def create_app(): from .system.database import db configure_app(app) db.init_app(app) - __load_auth(app) __load_plugins(app) @app.route("/", methods=["GET"]) diff --git a/flaschengeist/flaschengeist.example.cfg b/flaschengeist/flaschengeist.example.cfg index 4136f39..938602c 100644 --- a/flaschengeist/flaschengeist.example.cfg +++ b/flaschengeist/flaschengeist.example.cfg @@ -12,15 +12,21 @@ HOST = PASSWORD = DATABASE = -[MAIL] -URL = -PORT = -USER = -PASSWD = -MAIL = -CRYPT = SSL/STARTLS +[auth_plain] +enabled = true + +#[mail] +# enabled = true +# SERVER = +# PORT = +# USER = +# PASSWORD = +# MAIL = +# SSL or STARTLS +# CRYPT = SSL #[auth_ldap] +# enabled = true # URL = # PORT = # BINDDN = diff --git a/flaschengeist/modules/__init__.py b/flaschengeist/modules/__init__.py index d33f1cd..c4191be 100644 --- a/flaschengeist/modules/__init__.py +++ b/flaschengeist/modules/__init__.py @@ -1,5 +1,10 @@ +from pyhooks import precall_register + +send_message_hook = precall_register("send_message") + + class Plugin: - def __init__(self, blueprint, permissions = {}): + def __init__(self, config=None, blueprint=None, permissions={}): self.blueprint = blueprint self.permissions = permissions @@ -10,10 +15,7 @@ class Plugin: pass -class Auth: - def configure(self, config): - pass - +class AuthPlugin(Plugin): def login(self, user, pw): """ Login routine, MUST BE IMPLEMENTED! diff --git a/flaschengeist/modules/auth/__init__.py b/flaschengeist/modules/auth/__init__.py index ef93e97..c6e4238 100644 --- a/flaschengeist/modules/auth/__init__.py +++ b/flaschengeist/modules/auth/__init__.py @@ -14,12 +14,12 @@ from flaschengeist.system.decorator import login_required from flaschengeist.system.controller import accessTokenController, userController access_controller = LocalProxy(lambda: accessTokenController.AccessTokenController()) - auth_bp = Blueprint('auth', __name__) -def register(): - return Plugin(auth_bp) +class AuthRoutePlugin(Plugin): + def __init__(self, conf): + super().__init__(blueprint=auth_bp) ################################################# # Routes # @@ -31,84 +31,83 @@ def register(): # DELETE: logout / delete token # ################################################# + @auth_bp.route("/auth", methods=['POST']) + def _create_token(): + """ Login User -@auth_bp.route("/auth", methods=['POST']) -def _create_token(): - """ Login User + Login in User and create an AccessToken for the User. + Requires POST data {'userid': string, 'password': string} + Returns: + A JSON-File with user information and created token or errors + """ + logger.debug("Start log in.") + data = request.get_json() + try: + userid = data['userid'] + password = data['password'] + except KeyError: + raise BadRequest("Missing parameter(s)") - Login in User and create an AccessToken for the User. - Requires POST data {'userid': string, 'password': string} - Returns: - A JSON-File with user information and created token or errors - """ - logger.debug("Start log in.") - data = request.get_json() - try: - userid = data['userid'] - password = data['password'] - except KeyError: - raise BadRequest("Missing parameter(s)") + logger.debug("search user {{ {} }} in database".format(userid)) + user = userController.login_user(userid, password) + if not user: + raise Unauthorized + logger.debug("user is {{ {} }}".format(user)) + token = access_controller.create(user, user_agent=request.user_agent) + logger.debug("access token is {{ {} }}".format(token)) + logger.info("User {{ {} }} success login.".format(userid)) - logger.debug("search user {{ {} }} in database".format(userid)) - user = userController.login_user(userid, password) - if not user: - raise Unauthorized - logger.debug("user is {{ {} }}".format(user)) - token = access_controller.create(user, user_agent=request.user_agent) - logger.debug("access token is {{ {} }}".format(token)) - logger.info("User {{ {} }} success login.".format(userid)) - - # Lets cleanup the DB - access_controller.clear_expired() - return jsonify({"user": user, "token": token, "permissions": user.get_permissions()}) + # Lets cleanup the DB + access_controller.clear_expired() + return jsonify({"user": user, "token": token, "permissions": user.get_permissions()}) -@auth_bp.route("/auth", methods=['GET']) -@login_required() -def _get_tokens(access_token, **kwargs): - tokens = access_controller.get_users_tokens(access_token.user) - return jsonify(tokens) + @auth_bp.route("/auth", methods=['GET']) + @login_required() + def _get_tokens(access_token, **kwargs): + tokens = access_controller.get_users_tokens(access_token.user) + return jsonify(tokens) -@auth_bp.route("/auth/", methods=['DELETE']) -@login_required() -def _delete_token(token, access_token, **kwargs): - logger.debug("Try to delete access token {{ {} }}".format(token)) - token = access_controller.get_token(token, access_token.user) - if not token: - logger.debug("Token not found in database!") - # Return 403 error, so that users can not bruteforce tokens - # Valid tokens from other users and invalid tokens now are looking the same - raise Forbidden - access_controller.delete_token(token) - access_controller.clear_expired() - return jsonify({"ok": "ok"}) - - -@auth_bp.route("/auth/", methods=['GET']) -@login_required() -def _get_token(token, access_token, **kwargs): - logger.debug("get token {{ {} }}".format(token)) - token = access_controller.get_token(token, access_token.user) - if not token: - # Return 403 error, so that users can not bruteforce tokens - # Valid tokens from other users and invalid tokens now are looking the same - raise Forbidden - return jsonify(token) - - -@auth_bp.route("/auth/", methods=['PUT']) -@login_required() -def _set_lifetime(token, access_token, **kwargs): - token = access_controller.get_token(token, access_token.user) - if not token: - # Return 403 error, so that users can not bruteforce tokens - # Valid tokens from other users and invalid tokens now are looking the same - raise Forbidden - try: - lifetime = request.get_json()['value'] - logger.debug("set lifetime {{ {} }} to access token {{ {} }}".format(lifetime, token)) - access_controller.set_lifetime(token, lifetime) + @auth_bp.route("/auth/", methods=['DELETE']) + @login_required() + def _delete_token(token, access_token, **kwargs): + logger.debug("Try to delete access token {{ {} }}".format(token)) + token = access_controller.get_token(token, access_token.user) + if not token: + logger.debug("Token not found in database!") + # Return 403 error, so that users can not bruteforce tokens + # Valid tokens from other users and invalid tokens now are looking the same + raise Forbidden + access_controller.delete_token(token) + access_controller.clear_expired() return jsonify({"ok": "ok"}) - except (KeyError, TypeError): - raise BadRequest + + + @auth_bp.route("/auth/", methods=['GET']) + @login_required() + def _get_token(token, access_token, **kwargs): + logger.debug("get token {{ {} }}".format(token)) + token = access_controller.get_token(token, access_token.user) + if not token: + # Return 403 error, so that users can not bruteforce tokens + # Valid tokens from other users and invalid tokens now are looking the same + raise Forbidden + return jsonify(token) + + + @auth_bp.route("/auth/", methods=['PUT']) + @login_required() + def _set_lifetime(token, access_token, **kwargs): + token = access_controller.get_token(token, access_token.user) + if not token: + # Return 403 error, so that users can not bruteforce tokens + # Valid tokens from other users and invalid tokens now are looking the same + raise Forbidden + try: + lifetime = request.get_json()['value'] + logger.debug("set lifetime {{ {} }} to access token {{ {} }}".format(lifetime, token)) + access_controller.set_lifetime(token, lifetime) + return jsonify({"ok": "ok"}) + except (KeyError, TypeError): + raise BadRequest diff --git a/flaschengeist/modules/auth_ldap/__init__.py b/flaschengeist/modules/auth_ldap/__init__.py index 3f3c5c4..909ae26 100644 --- a/flaschengeist/modules/auth_ldap/__init__.py +++ b/flaschengeist/modules/auth_ldap/__init__.py @@ -1,29 +1,26 @@ -from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError +import ssl from ldap3.utils.hashed import hashed -from werkzeug.exceptions import BadRequest - -import flaschengeist.modules as modules +from ldap3 import SUBTREE, MODIFY_REPLACE, HASHED_SALTED_SHA512 +from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError from flask import current_app as app from flask_ldapconn import LDAPConn -from ldap3 import SUBTREE, MODIFY_REPLACE, HASHED_SALTED_SHA512 -import ssl +from werkzeug.exceptions import BadRequest +from flaschengeist.modules import AuthPlugin from flaschengeist.system.models.user import User -from flaschengeist import logger -class AuthLDAP(modules.Auth): - _default = { - 'PORT': '389', - 'USE_SSL': 'False' - } - ldap = None - dn = None +class AuthLDAP(AuthPlugin): + def __init__(self, config): + super().__init__() - def configure(self, config): - for name in self._default: + defaults = { + 'PORT': '389', + 'USE_SSL': 'False' + } + for name in defaults: if name not in config: - config[name] = self._default[name] + config[name] = defaults[name] app.config.update( LDAP_SERVER=config['URL'], diff --git a/flaschengeist/modules/message_mail.py b/flaschengeist/modules/message_mail.py new file mode 100644 index 0000000..ead63ec --- /dev/null +++ b/flaschengeist/modules/message_mail.py @@ -0,0 +1,43 @@ +import smtplib +from email.mime.multipart import MIMEMultipart + +from flaschengeist.system.models.user import User +from flaschengeist.system.controller import userController +from flaschengeist.system.controller.messageController import Message + +from . import Plugin, send_message_hook + + +class MailMessagePlugin(Plugin): + def __init__(self, config): + super().__init__() + self.server = config['SERVER'] + self.port = config['PORT'] + self.user = config['USER'] + self.password = config['PASSWORD'] + self.crypt = config['CRYPT'] + self.mail = config['MAIL'] + + @send_message_hook + def send_mail(self, msg: Message): + if isinstance(msg.receiver, User): + recipients = [msg.receiver.mail] + else: + recipients = userController.get_user_by_role(msg.receiver) + + mail = MIMEMultipart() + mail['From'] = self.mail + mail['To'] = ", ".join(recipients) + mail['Subject'] = msg.subject + msg.attach(msg.message) + if not self.smtp: + self.__connect() + self.smtp.sendmail(self.mail, recipients, msg.as_string()) + + def __connect(self): + if self.crypt == 'SSL': + self.smtp = smtplib.SMTP_SSL(self.server, self.port) + if self.crypt == 'STARTTLS': + self.smtp = smtplib.SMTP(self.smtpServer, self.port) + self.smtp.starttls() + self.smtp.login(self.user, self.password) diff --git a/flaschengeist/modules/roles/__init__.py b/flaschengeist/modules/roles/__init__.py index b29363f..d6ff05a 100644 --- a/flaschengeist/modules/roles/__init__.py +++ b/flaschengeist/modules/roles/__init__.py @@ -1,16 +1,16 @@ from flask import Blueprint, request, jsonify -from werkzeug.exceptions import NotFound, BadRequest, Forbidden +from werkzeug.exceptions import NotFound, BadRequest from flaschengeist.modules import Plugin from flaschengeist.system.decorator import login_required from flaschengeist.system.controller import roleController roles_bp = Blueprint("roles", __name__) -permissions = {} -def register(): - return Plugin(roles_bp, permissions) +class RolesPlugin(Plugin): + def __init__(self, config): + super().__init__(config, roles_bp) ###################################################### # Routes # @@ -23,66 +23,60 @@ def register(): # DELETE: remove role # ###################################################### + @roles_bp.route("/roles", methods=['POST']) + @login_required() + def add_role(self): + data = request.get_json() + if not data or "name" not in data: + raise BadRequest + if "permissions" in data: + permissions = data["permissions"] + role = roleController.create_role(data["name"], permissions) + return jsonify({"ok": "ok", "id": role.id}) -@roles_bp.route("/roles", methods=['POST']) -@login_required() -def __add_role(): - data = request.get_json() - if not data or "name" not in data: - raise BadRequest - if "permissions" in data: - permissions = data["permissions"] - role = roleController.create_role(data["name"], permissions) - return jsonify({"ok": "ok", "id": role.id}) + @roles_bp.route("/roles", methods=['GET']) + @login_required() + def list_roles(self, **kwargs): + roles = roleController.get_roles() + return jsonify(roles) + @roles_bp.route("/roles/permissions", methods=['GET']) + @login_required() + def list_permissions(self, **kwargs): + permissions = roleController.get_permissions() + return jsonify(permissions) -@roles_bp.route("/roles", methods=['GET']) -@login_required() -def __list_roles(**kwargs): - roles = roleController.get_roles() - return jsonify(roles) - - -@roles_bp.route("/roles/permissions", methods=['GET']) -@login_required() -def __list_permissions(**kwargs): - permissions = roleController.get_permissions() - return jsonify(permissions) - - -@roles_bp.route("/roles/", methods=['GET']) -@login_required() -def __get_role(rid, **kwargs): - role = roleController.get_role(rid) - if role: - return jsonify({ - "id": role.id, - "name": role, - "permissions": role.permissions - }) - raise NotFound - - -@roles_bp.route("/roles/", methods=['PUT']) -@login_required() -def __edit_role(rid, **kwargs): - role = roleController.get_role(rid) - if not role: + @roles_bp.route("/roles/", methods=['GET']) + @login_required() + def __get_role(self, rid, **kwargs): + role = roleController.get_role(rid) + if role: + return jsonify({ + "id": role.id, + "name": role, + "permissions": role.permissions + }) raise NotFound - data = request.get_json() - if 'name' in data: - role.name = data["name"] - if "permissions" in data: - roleController.set_permissions(role, data["permissions"]) - roleController.update_role(role) - return jsonify({"ok": "ok"}) + @roles_bp.route("/roles/", methods=['PUT']) + @login_required() + def __edit_role(self, rid, **kwargs): + role = roleController.get_role(rid) + if not role: + raise NotFound + data = request.get_json() + if 'name' in data: + role.name = data["name"] + if "permissions" in data: + roleController.set_permissions(role, data["permissions"]) + roleController.update_role(role) + return jsonify({"ok": "ok"}) -@roles_bp.route("/roles/", methods=['DELETE']) -@login_required() -def __delete_role(rid, **kwargs): - if not roleController.delete_role(rid): - raise NotFound + @roles_bp.route("/roles/", methods=['DELETE']) + @login_required() + def __delete_role(self, rid, **kwargs): + if not roleController.delete_role(rid): + raise NotFound - return jsonify({"ok": "ok"}) + return jsonify({"ok": "ok"}) diff --git a/flaschengeist/modules/schedule/__init__.py b/flaschengeist/modules/schedule/__init__.py index 62bf9cd..be51c08 100644 --- a/flaschengeist/modules/schedule/__init__.py +++ b/flaschengeist/modules/schedule/__init__.py @@ -1,21 +1,20 @@ from dateutil import parser +from datetime import datetime, timedelta from flask import Blueprint, request, jsonify from werkzeug.exceptions import BadRequest, NotFound -from datetime import datetime, timedelta from flaschengeist.modules import Plugin -from flaschengeist.system.controller import eventController from flaschengeist.system.database import db -from flaschengeist.system.decorator import login_required from flaschengeist.system.models.event import EventKind +from flaschengeist.system.decorator import login_required +from flaschengeist.system.controller import eventController schedule_bp = Blueprint("schedule", __name__, url_prefix="/schedule") -permissions = {} -def register(): - return Plugin(schedule_bp, permissions) - +class SchedulePlugin(Plugin): + def __init__(self, config): + super().__init__(blueprint=schedule_bp) #################################################################################### # Routes # @@ -38,161 +37,159 @@ def register(): # DELETE: remove user # #################################################################################### + @schedule_bp.route("/events/", methods=['GET']) + @login_required() # roles=['schedule_read']) + def __get_event(self, id, **kwargs): + event = eventController.get_event(id) + if not event: + raise NotFound + return jsonify(event) -@schedule_bp.route("/events/", methods=['GET']) -@login_required() # roles=['schedule_read']) -def __get_event(id, **kwargs): - event = eventController.get_event(id) - if not event: - raise NotFound - return jsonify(event) + @schedule_bp.route("/events", methods=['GET']) + @schedule_bp.route("/events//", methods=['GET']) + @schedule_bp.route("/events///", methods=['GET']) + @login_required() # roles=['schedule_read']) + def __get_events(self, year=datetime.now().year, month=datetime.now().month, day=None, **kwrags): + """Get Event objects for specified date (or month or year), + if nothing set then events for current month are returned - -@schedule_bp.route("/events", methods=['GET']) -@schedule_bp.route("/events//", methods=['GET']) -@schedule_bp.route("/events///", methods=['GET']) -@login_required() # roles=['schedule_read']) -def __get_events(year=datetime.now().year, month=datetime.now().month, day=None, **kwrags): - """Get Event objects for specified date (or month or year), - if nothing set then events for current month are returned - - Args: - year (int, optional): year to query, defaults to current year - month (int, optional): month to query (if set), defaults to current month - day (int, optional): day to query events for (if set) - **kwrags: contains at least access_token (see flaschengeist.decorator) - Returns: - JSON list containing events found - Raises: - BadRequest: If date is invalid - """ - try: - begin = datetime(year=year, month=month, day=1) - if day: - begin += timedelta(days=day - 1) - end = begin + timedelta(days=1) - else: - if month == 12: - end = datetime(year=year + 1, month=1, day=1) + Args: + year (int, optional): year to query, defaults to current year + month (int, optional): month to query (if set), defaults to current month + day (int, optional): day to query events for (if set) + **kwrags: contains at least access_token (see flaschengeist.decorator) + Returns: + JSON list containing events found + Raises: + BadRequest: If date is invalid + """ + try: + begin = datetime(year=year, month=month, day=1) + if day: + begin += timedelta(days=day - 1) + end = begin + timedelta(days=1) else: - end = datetime(year=year, month=month+1, day=1) + if month == 12: + end = datetime(year=year + 1, month=1, day=1) + else: + end = datetime(year=year, month=month+1, day=1) - events = eventController.get_events(begin, end) - return jsonify(events) - except ValueError: - raise BadRequest("Invalid date given") + events = eventController.get_events(begin, end) + return jsonify(events) + except ValueError: + raise BadRequest("Invalid date given") -@schedule_bp.route("/eventKinds", methods=['POST']) -@login_required() -def __new_event_kind(**kwargs): - data = request.get_json() - if "name" not in data: - raise BadRequest - kind = eventController.create_event_kind(data["name"]) - return jsonify({"ok": "ok", "id": kind.id}) + @schedule_bp.route("/eventKinds", methods=['POST']) + @login_required() + def __new_event_kind(self, **kwargs): + data = request.get_json() + if "name" not in data: + raise BadRequest + kind = eventController.create_event_kind(data["name"]) + return jsonify({"ok": "ok", "id": kind.id}) -@schedule_bp.route("/slotKinds", methods=["POST"]) -@login_required() -def __new_slot_kind(**kwargs): - data = request.get_json() - if not data or "name" not in data: - raise BadRequest - kind = eventController.create_job_kind(data["name"]) - return jsonify({"ok": "ok", "id": kind.id}) + @schedule_bp.route("/slotKinds", methods=["POST"]) + @login_required() + def __new_slot_kind(self, **kwargs): + data = request.get_json() + if not data or "name" not in data: + raise BadRequest + kind = eventController.create_job_kind(data["name"]) + return jsonify({"ok": "ok", "id": kind.id}) -@schedule_bp.route("/events", methods=['POST']) -@login_required() -def __new_event(**kwargs): - data = request.get_json() - event = eventController.create_event(begin=parser.isoparse(data["begin"]), - end=parser.isoparse(data["end"]), - description=data["description"], - kind=EventKind.query.get(data["kind"])) - return jsonify({"ok": "ok", "id": event.id}) + @schedule_bp.route("/events", methods=['POST']) + @login_required() + def __new_event(self, **kwargs): + data = request.get_json() + event = eventController.create_event(begin=parser.isoparse(data["begin"]), + end=parser.isoparse(data["end"]), + description=data["description"], + kind=EventKind.query.get(data["kind"])) + return jsonify({"ok": "ok", "id": event.id}) -@schedule_bp.route("/events/", methods=["DELETE"]) -@login_required() -def __delete_event(id, **kwargs): - if not eventController.delete_event(id): - raise NotFound - db.session.commit() - return jsonify({'ok': 'ok'}) + @schedule_bp.route("/events/", methods=["DELETE"]) + @login_required() + def __delete_event(self, id, **kwargs): + if not eventController.delete_event(id): + raise NotFound + db.session.commit() + return jsonify({'ok': 'ok'}) -@schedule_bp.route("/eventKinds/", methods=["PUT"]) -@login_required() -def __edit_event_kind(id, **kwargs): - data = request.get_json() - if not data or "name" not in data: - raise BadRequest - eventController.rename_event_kind(id, data["name"]) - return jsonify({"ok": "ok"}) - - -@schedule_bp.route("/events//slots", methods=["GET"]) -@login_required() -def __get_slots(event_id, **kwargs): - event = eventController.get_event(event_id) - if not event: - raise NotFound - return jsonify({event.slots}) - - -@schedule_bp.route("/events//slots/", methods=["GET"]) -@login_required() -def __get_slot(event_id, slot_id, **kwargs): - slot = eventController.get_event_slot(slot_id, event_id) - if slot: - return jsonify(slot) - raise NotFound - - -@schedule_bp.route("/events//slots/", methods=["DELETE"]) -@login_required() -def __delete_slot(event_id, slot_id, **kwargs): - if eventController.delete_event_slot(slot_id, event_id): + @schedule_bp.route("/eventKinds/", methods=["PUT"]) + @login_required() + def __edit_event_kind(self, id, **kwargs): + data = request.get_json() + if not data or "name" not in data: + raise BadRequest + eventController.rename_event_kind(id, data["name"]) return jsonify({"ok": "ok"}) - raise NotFound -@schedule_bp.route("/events//slots/", methods=["PUT"]) -@login_required() -def __update_slot(event_id, slot_id, **kwargs): - data = request.get_json() - if not data: - raise BadRequest - - for job in data['jobs']: - eventController.add_job(job.kind, job.user) - if eventController.delete_event_slot(slot_id, event_id): - return jsonify({"ok": "ok"}) - raise NotFound + @schedule_bp.route("/events//slots", methods=["GET"]) + @login_required() + def __get_slots(self, event_id, **kwargs): + event = eventController.get_event(event_id) + if not event: + raise NotFound + return jsonify({event.slots}) -@schedule_bp.route("/events//slots", methods=["POST"]) -@login_required() -def __add_slot(event_id, **kwargs): - event = eventController.get_event(event_id) - if not event: + @schedule_bp.route("/events//slots/", methods=["GET"]) + @login_required() + def __get_slot(self, event_id, slot_id, **kwargs): + slot = eventController.get_event_slot(slot_id, event_id) + if slot: + return jsonify(slot) raise NotFound - data = request.get_json() - attr = {"job_slots": []} - try: - if "start" in data: - attr["start"] = parser.isoparse(data["start"]) - if "end" in data: - attr["end"] = parser.isoparse(data["end"]) - for job in data["jobs"]: - attr["job_slots"].append({"needed_persons": job["needed_persons"], "kind": job["kind"]}) - except KeyError: - raise BadRequest("Missing data in request") - eventController.add_slot(event, **attr) - return jsonify({"ok": "ok"}) -def __edit_event(): - ... + @schedule_bp.route("/events//slots/", methods=["DELETE"]) + @login_required() + def __delete_slot(self, event_id, slot_id, **kwargs): + if eventController.delete_event_slot(slot_id, event_id): + return jsonify({"ok": "ok"}) + raise NotFound + + + @schedule_bp.route("/events//slots/", methods=["PUT"]) + @login_required() + def __update_slot(self, event_id, slot_id, **kwargs): + data = request.get_json() + if not data: + raise BadRequest + + for job in data['jobs']: + eventController.add_job(job.kind, job.user) + if eventController.delete_event_slot(slot_id, event_id): + return jsonify({"ok": "ok"}) + raise NotFound + + + @schedule_bp.route("/events//slots", methods=["POST"]) + @login_required() + def __add_slot(self, event_id, **kwargs): + event = eventController.get_event(event_id) + if not event: + raise NotFound + data = request.get_json() + attr = {"job_slots": []} + try: + if "start" in data: + attr["start"] = parser.isoparse(data["start"]) + if "end" in data: + attr["end"] = parser.isoparse(data["end"]) + for job in data["jobs"]: + attr["job_slots"].append({"needed_persons": job["needed_persons"], "kind": job["kind"]}) + except KeyError: + raise BadRequest("Missing data in request") + eventController.add_slot(event, **attr) + return jsonify({"ok": "ok"}) + + + def __edit_event(self): + ... diff --git a/flaschengeist/modules/users/__init__.py b/flaschengeist/modules/users/__init__.py index d440750..53d3aef 100644 --- a/flaschengeist/modules/users/__init__.py +++ b/flaschengeist/modules/users/__init__.py @@ -10,8 +10,9 @@ users_bp = Blueprint("users", __name__) permissions = {'EDIT_USER': 'edit_user'} -def register(): - return Plugin(users_bp, permissions) +class UsersPlugin(Plugin): + def __init__(self, config): + super().__init__(blueprint=users_bp, permissions=permissions) ################################################# # Routes # @@ -23,49 +24,45 @@ def register(): # DELETE: remove user # ################################################# + @users_bp.route("/users", methods=['POST']) + def __registration(self): + logger.debug("Register new User...") + return jsonify({"ok": "ok... well not implemented"}) -@users_bp.route("/users", methods=['POST']) -def __registration(): - logger.debug("Register new User...") - return jsonify({"ok": "ok... well not implemented"}) + @users_bp.route("/users", methods=['GET']) + @login_required() + def __list_users(self, **kwargs): + logger.debug("Retrieve list of all users") + users = userController.get_users() + return jsonify(users) - -@users_bp.route("/users", methods=['GET']) -@login_required() -def __list_users(**kwargs): - logger.debug("Retrieve list of all users") - users = userController.get_users() - return jsonify(users) - - -@users_bp.route("/users/", methods=['GET']) -@login_required() -def __get_user(uid, **kwargs): - logger.debug("Get information of user {{ {} }}".format(uid)) - user = userController.get_user(uid) - if user: - return jsonify(user) - raise NotFound - - -@users_bp.route("/users/", methods=['PUT']) -@login_required() -def __edit_user(uid, **kwargs): - logger.debug("Modify information of user {{ {} }}".format(uid)) - user = userController.get_user(uid) - if not user: + @users_bp.route("/users/", methods=['GET']) + @login_required() + def __get_user(self, uid, **kwargs): + logger.debug("Get information of user {{ {} }}".format(uid)) + user = userController.get_user(uid) + if user: + return jsonify(user) raise NotFound - if uid != kwargs['access_token'].user.uid and user.has_permissions(permissions['EDIT_USER']): - return Forbidden + @users_bp.route("/users/", methods=['PUT']) + @login_required() + def __edit_user(self, uid, **kwargs): + logger.debug("Modify information of user {{ {} }}".format(uid)) + user = userController.get_user(uid) + if not user: + raise NotFound - data = request.get_json() - if 'password' not in data: - raise BadRequest("Password is missing") - for key in ["firstname", "lastname", "display_name", "mail"]: - if key in data: - setattr(user, key, data[key]) - new_password = data['new_password'] if 'new_password' in data else None - userController.modify_user(user, data['password'], new_password) - userController.update_user(user) - return jsonify({"ok": "ok"}) + if uid != kwargs['access_token'].user.uid and user.has_permissions(permissions['EDIT_USER']): + return Forbidden + + data = request.get_json() + if 'password' not in data: + raise BadRequest("Password is missing") + for key in ["firstname", "lastname", "display_name", "mail"]: + if key in data: + setattr(user, key, data[key]) + new_password = data['new_password'] if 'new_password' in data else None + userController.modify_user(user, data['password'], new_password) + userController.update_user(user) + return jsonify({"ok": "ok"}) diff --git a/flaschengeist/system/controller/emailController.py b/flaschengeist/system/controller/emailController.py deleted file mode 100644 index 067a34d..0000000 --- a/flaschengeist/system/controller/emailController.py +++ /dev/null @@ -1,119 +0,0 @@ -import smtplib -from datetime import datetime -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.header import Header -from geruecht.logger import getDebugLogger -from . import mailConfig - -debug = getDebugLogger() - -class EmailController(): - - def __init__(self): - debug.info("init email controller") - self.smtpServer = mailConfig['URL'] - self.port = mailConfig['port'] - self.user = mailConfig['user'] - self.passwd = mailConfig['passwd'] - self.crypt = mailConfig['crypt'] - self.email = mailConfig['email'] - - debug.debug("smtpServer is {{ {} }}, port is {{ {} }}, user is {{ {} }}, crypt is {{ {} }}, email is {{ {} }}".format(self.smtpServer, self.port, self.user, self.crypt, self.email)) - - def __connect__(self): - debug.info('connect to email server') - if self.crypt == 'SSL': - self.smtp = smtplib.SMTP_SSL(self.smtpServer, self.port) - log = self.smtp.ehlo() - debug.debug("ehlo is {{ {} }}".format(log)) - if self.crypt == 'STARTTLS': - self.smtp = smtplib.SMTP(self.smtpServer, self.port) - log = self.smtp.ehlo() - debug.debug("ehlo is {{ {} }}".format(log)) - log = self.smtp.starttls() - debug.debug("starttles is {{ {} }}".format(log)) - log = self.smtp.login(self.user, self.passwd) - debug.debug("login is {{ {} }}".format(log)) - - def jobTransact(self, user, jobtransact): - debug.info("create email jobtransact {{ {} }}for user {{ {} }}".format(jobtransact, user)) - date = '{}.{}.{}'.format(jobtransact['on_date']['day'], jobtransact['on_date']['month'], jobtransact['on_date']['year']) - from_user = '{} {}'.format(jobtransact['from_user']['firstname'], jobtransact['from_user']['lastname']) - job_kind = jobtransact['job_kind'] - subject = 'Dienstanfrage am {}'.format(date) - text = MIMEText( - "Hallo {} {},\n" - "{} fragt, ob du am {} den Dienst {} übernehmen willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date, job_kind['name']), 'plain') - debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) - return (subject, text) - - def jobInvite(self, user, jobtransact): - debug.info("create email jobtransact {{ {} }}for user {{ {} }}".format(jobtransact, user)) - date = '{}.{}.{}'.format(jobtransact['on_date']['day'], jobtransact['on_date']['month'], jobtransact['on_date']['year']) - from_user = '{} {}'.format(jobtransact['from_user']['firstname'], jobtransact['from_user']['lastname']) - subject = 'Diensteinladung am {}'.format(date) - text = MIMEText( - "Hallo {} {},\n" - "{} fragt, ob du am {} mit Dienst haben willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date), 'plain') - debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) - return (subject, text) - - def credit(self, user): - debug.info("create email credit for user {{ {} }}".format(user)) - subject = Header('Gerücht, bezahle deine Schulden!', 'utf-8') - sum = user.getGeruecht(datetime.now().year).getSchulden() - if sum < 0: - type = 'Schulden' - add = 'Bezahle diese umgehend an den Finanzer.' - else: - type = 'Guthaben' - add = '' - text = MIMEText( - "Hallo {} {},\nDu hast {} im Wert von {:.2f} €. {}\n\nDiese Nachricht wurde automatisch erstellt.".format( - user.firstname, user.lastname, type, abs(sum) / 100, add), 'plain') - debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) - return (subject, text) - - def passwordReset(self, user, data): - debug.info("create email passwort reset for user {{ {} }}".format(user)) - subject = Header("Password vergessen") - text = MIMEText( - "Hallo {} {},\nDu hast dein Password vergessen!\nDies wurde nun mit Flaschengeist zurückgesetzt.\nDein neues Passwort lautet:\n{}\n\nBitte ändere es sofort in deinem Flaschengeistprolif in https://flaschengeist.wu5.de.".format( - user.firstname, user.lastname, data['password'] - ), 'plain' - ) - debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) - return (subject, text) - - def sendMail(self, user, type='credit', jobtransact=None, **kwargs): - debug.info("send email to user {{ {} }}".format(user)) - try: - if user.mail == 'None' or not user.mail: - debug.warning("user {{ {} }} has no email-address".format(user)) - raise Exception("no valid Email") - msg = MIMEMultipart() - msg['From'] = self.email - msg['To'] = user.mail - - if type == 'credit': - subject, text = self.credit(user) - elif type == 'jobtransact': - subject, text = self.jobTransact(user, jobtransact) - elif type == 'jobinvite': - subject, text = self.jobInvite(user, jobtransact) - elif type == 'passwordReset': - subject, text = self.passwordReset(user, kwargs) - else: - raise Exception("Fail to send Email. No type is set. user={}, type={} , jobtransact={}".format(user, type, jobtransact)) - - msg['Subject'] = subject - msg.attach(text) - - debug.debug("send email {{ {} }} to user {{ {} }}".format(msg.as_string(), user)) - self.__connect__() - self.smtp.sendmail(self.email, user.mail, msg.as_string()) - return {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} - except Exception: - debug.warning("exception in send email", exc_info=True) - return {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} diff --git a/flaschengeist/system/controller/eventController.py b/flaschengeist/system/controller/eventController.py index 85c1a52..7c38bed 100644 --- a/flaschengeist/system/controller/eventController.py +++ b/flaschengeist/system/controller/eventController.py @@ -1,10 +1,9 @@ from werkzeug.exceptions import BadRequest, NotFound - -from flaschengeist.system.models.event import EventKind, Event, EventSlot, JobSlot, JobKind from sqlalchemy.exc import IntegrityError from flaschengeist import logger from flaschengeist.system.database import db +from flaschengeist.system.models.event import EventKind, Event, EventSlot, JobSlot, JobKind def get_event(id): diff --git a/flaschengeist/system/controller/messageController.py b/flaschengeist/system/controller/messageController.py new file mode 100644 index 0000000..1f064d6 --- /dev/null +++ b/flaschengeist/system/controller/messageController.py @@ -0,0 +1,14 @@ +from flaschengeist.system.models.user import User, Role +from pyhooks import Hook + + +class Message: + def __init__(self, receiver: User or Role, message: str, subject: str): + self.message = message + self.subject = subject + self.receiver = receiver + + +@Hook +def send_message(message: Message): + pass diff --git a/flaschengeist/system/controller/userController.py b/flaschengeist/system/controller/userController.py index 049e2f4..03720c5 100644 --- a/flaschengeist/system/controller/userController.py +++ b/flaschengeist/system/controller/userController.py @@ -1,6 +1,6 @@ from flask import current_app -from flaschengeist.system.models.user import User +from flaschengeist.system.models.user import User, Role from flaschengeist.system.database import db from flaschengeist import logger @@ -44,5 +44,9 @@ def get_users(): return User.query.all() +def get_user_by_role(role: Role): + return User.query.join(User.roles).filter_by(role_id=role.id).all() + + def get_user(uid): return User.query.filter(User.uid == uid).one_or_none() diff --git a/setup.py b/setup.py index 706a52b..1b967fe 100644 --- a/setup.py +++ b/setup.py @@ -18,17 +18,18 @@ setup( "flask_cors", "werkzeug", "bjoern", - "python-dateutil" + "python-dateutil", + "pyhooks" ], extras_require={"ldap": ["flask_ldapconn", "ldap3"]}, entry_points={ "flaschengeist.plugin": [ - "auth = flaschengeist.modules.auth:register", - "users = flaschengeist.modules.users:register", - "roles = flaschengeist.modules.roles:register", - "schedule = flaschengeist.modules.schedule:register", - ], - "flaschengeist.auth": [ + "auth = flaschengeist.modules.auth:AuthRoutePlugin", + "users = flaschengeist.modules.users:UsersPlugin", + "roles = flaschengeist.modules.roles:RolesPlugin", + "schedule = flaschengeist.modules.schedule:SchedulePlugin", + "mail = flaschengeist.modules.message_mail:MailMessagePlugin", + "auth_plain = flaschengeist.modules.auth_plain:AuthPlain", "auth_ldap = flaschengeist.modules.auth_ldap:AuthLDAP [ldap]", ],