Only use one plugin system, load auth and "normal" plugins at once.

* Added Plugin class, where to inheritate from
This commit is contained in:
Ferdinand Thiessen 2020-10-15 21:58:56 +02:00
parent 60c2637784
commit 2c55edf6a8
14 changed files with 428 additions and 497 deletions

View File

@ -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()()
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("Loaded plugin >{}<".format(entry_point.name))
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"])

View File

@ -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 =

View File

@ -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!

View File

@ -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,7 +31,6 @@ def register():
# DELETE: logout / delete token #
#################################################
@auth_bp.route("/auth", methods=['POST'])
def _create_token():
""" Login User

View File

@ -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 = {
class AuthLDAP(AuthPlugin):
def __init__(self, config):
super().__init__()
defaults = {
'PORT': '389',
'USE_SSL': 'False'
}
ldap = None
dn = None
def configure(self, config):
for name in self._default:
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'],

View File

@ -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)

View File

@ -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,10 +23,9 @@ def register():
# DELETE: remove role #
######################################################
@roles_bp.route("/roles", methods=['POST'])
@login_required()
def __add_role():
def add_role(self):
data = request.get_json()
if not data or "name" not in data:
raise BadRequest
@ -35,24 +34,21 @@ def __add_role():
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(**kwargs):
def list_roles(self, **kwargs):
roles = roleController.get_roles()
return jsonify(roles)
@roles_bp.route("/roles/permissions", methods=['GET'])
@login_required()
def __list_permissions(**kwargs):
def list_permissions(self, **kwargs):
permissions = roleController.get_permissions()
return jsonify(permissions)
@roles_bp.route("/roles/<rid>", methods=['GET'])
@login_required()
def __get_role(rid, **kwargs):
def __get_role(self, rid, **kwargs):
role = roleController.get_role(rid)
if role:
return jsonify({
@ -62,10 +58,9 @@ def __get_role(rid, **kwargs):
})
raise NotFound
@roles_bp.route("/roles/<rid>", methods=['PUT'])
@login_required()
def __edit_role(rid, **kwargs):
def __edit_role(self, rid, **kwargs):
role = roleController.get_role(rid)
if not role:
raise NotFound
@ -78,10 +73,9 @@ def __edit_role(rid, **kwargs):
roleController.update_role(role)
return jsonify({"ok": "ok"})
@roles_bp.route("/roles/<rid>", methods=['DELETE'])
@login_required()
def __delete_role(rid, **kwargs):
def __delete_role(self, rid, **kwargs):
if not roleController.delete_role(rid):
raise NotFound

View File

@ -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,21 +37,19 @@ def register():
# DELETE: remove user #
####################################################################################
@schedule_bp.route("/events/<int:id>", methods=['GET'])
@login_required() # roles=['schedule_read'])
def __get_event(id, **kwargs):
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'])
@schedule_bp.route("/events/<int:year>/<int:month>", methods=['GET'])
@schedule_bp.route("/events/<int:year>/<int:month>/<int:day>", methods=['GET'])
@login_required() # roles=['schedule_read'])
def __get_events(year=datetime.now().year, month=datetime.now().month, day=None, **kwrags):
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
@ -85,7 +82,7 @@ def __get_events(year=datetime.now().year, month=datetime.now().month, day=None,
@schedule_bp.route("/eventKinds", methods=['POST'])
@login_required()
def __new_event_kind(**kwargs):
def __new_event_kind(self, **kwargs):
data = request.get_json()
if "name" not in data:
raise BadRequest
@ -95,7 +92,7 @@ def __new_event_kind(**kwargs):
@schedule_bp.route("/slotKinds", methods=["POST"])
@login_required()
def __new_slot_kind(**kwargs):
def __new_slot_kind(self, **kwargs):
data = request.get_json()
if not data or "name" not in data:
raise BadRequest
@ -105,7 +102,7 @@ def __new_slot_kind(**kwargs):
@schedule_bp.route("/events", methods=['POST'])
@login_required()
def __new_event(**kwargs):
def __new_event(self, **kwargs):
data = request.get_json()
event = eventController.create_event(begin=parser.isoparse(data["begin"]),
end=parser.isoparse(data["end"]),
@ -116,7 +113,7 @@ def __new_event(**kwargs):
@schedule_bp.route("/events/<int:id>", methods=["DELETE"])
@login_required()
def __delete_event(id, **kwargs):
def __delete_event(self, id, **kwargs):
if not eventController.delete_event(id):
raise NotFound
db.session.commit()
@ -125,7 +122,7 @@ def __delete_event(id, **kwargs):
@schedule_bp.route("/eventKinds/<int:id>", methods=["PUT"])
@login_required()
def __edit_event_kind(id, **kwargs):
def __edit_event_kind(self, id, **kwargs):
data = request.get_json()
if not data or "name" not in data:
raise BadRequest
@ -135,7 +132,7 @@ def __edit_event_kind(id, **kwargs):
@schedule_bp.route("/events/<int:event_id>/slots", methods=["GET"])
@login_required()
def __get_slots(event_id, **kwargs):
def __get_slots(self, event_id, **kwargs):
event = eventController.get_event(event_id)
if not event:
raise NotFound
@ -144,7 +141,7 @@ def __get_slots(event_id, **kwargs):
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["GET"])
@login_required()
def __get_slot(event_id, slot_id, **kwargs):
def __get_slot(self, event_id, slot_id, **kwargs):
slot = eventController.get_event_slot(slot_id, event_id)
if slot:
return jsonify(slot)
@ -153,7 +150,7 @@ def __get_slot(event_id, slot_id, **kwargs):
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["DELETE"])
@login_required()
def __delete_slot(event_id, slot_id, **kwargs):
def __delete_slot(self, event_id, slot_id, **kwargs):
if eventController.delete_event_slot(slot_id, event_id):
return jsonify({"ok": "ok"})
raise NotFound
@ -161,7 +158,7 @@ def __delete_slot(event_id, slot_id, **kwargs):
@schedule_bp.route("/events/<int:event_id>/slots/<int:slot_id>", methods=["PUT"])
@login_required()
def __update_slot(event_id, slot_id, **kwargs):
def __update_slot(self, event_id, slot_id, **kwargs):
data = request.get_json()
if not data:
raise BadRequest
@ -175,7 +172,7 @@ def __update_slot(event_id, slot_id, **kwargs):
@schedule_bp.route("/events/<int:event_id>/slots", methods=["POST"])
@login_required()
def __add_slot(event_id, **kwargs):
def __add_slot(self, event_id, **kwargs):
event = eventController.get_event(event_id)
if not event:
raise NotFound
@ -194,5 +191,5 @@ def __add_slot(event_id, **kwargs):
return jsonify({"ok": "ok"})
def __edit_event():
def __edit_event(self):
...

View File

@ -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,34 +24,30 @@ def register():
# DELETE: remove user #
#################################################
@users_bp.route("/users", methods=['POST'])
def __registration():
def __registration(self):
logger.debug("Register new User...")
return jsonify({"ok": "ok... well not implemented"})
@users_bp.route("/users", methods=['GET'])
@login_required()
def __list_users(**kwargs):
def __list_users(self, **kwargs):
logger.debug("Retrieve list of all users")
users = userController.get_users()
return jsonify(users)
@users_bp.route("/users/<uid>", methods=['GET'])
@login_required()
def __get_user(uid, **kwargs):
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
@users_bp.route("/users/<uid>", methods=['PUT'])
@login_required()
def __edit_user(uid, **kwargs):
def __edit_user(self, uid, **kwargs):
logger.debug("Modify information of user {{ {} }}".format(uid))
user = userController.get_user(uid)
if not user:

View File

@ -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}}

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -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]",
],