diff --git a/flaschengeist/alembic/migrations/255b93b6beed_flaschengeist_initial.py b/flaschengeist/alembic/migrations/20482a003db8_initial_core_db.py similarity index 57% rename from flaschengeist/alembic/migrations/255b93b6beed_flaschengeist_initial.py rename to flaschengeist/alembic/migrations/20482a003db8_initial_core_db.py index b18a71e..a2a2445 100644 --- a/flaschengeist/alembic/migrations/255b93b6beed_flaschengeist_initial.py +++ b/flaschengeist/alembic/migrations/20482a003db8_initial_core_db.py @@ -1,8 +1,8 @@ -"""Flaschengeist: Initial +"""Initial core db -Revision ID: 255b93b6beed +Revision ID: 20482a003db8 Revises: -Create Date: 2022-02-23 14:33:02.851388 +Create Date: 2022-08-25 15:13:34.900996 """ from alembic import op @@ -11,24 +11,17 @@ import flaschengeist # revision identifiers, used by Alembic. -revision = "255b93b6beed" +revision = "20482a003db8" down_revision = None branch_labels = ("flaschengeist",) depends_on = None def upgrade(): - op.create_table( - "plugin_setting", - sa.Column("id", flaschengeist.models.Serial(), nullable=False), - sa.Column("plugin", sa.String(length=127), nullable=True), - sa.Column("name", sa.String(length=127), nullable=False), - sa.Column("value", sa.PickleType(protocol=4), nullable=True), - sa.PrimaryKeyConstraint("id", name=op.f("pk_plugin_setting")), - ) + # ### commands auto generated by Alembic - please adjust! ### op.create_table( "image", - sa.Column("id", flaschengeist.models.Serial(), nullable=False), + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), sa.Column("filename", sa.String(length=255), nullable=False), sa.Column("mimetype", sa.String(length=127), nullable=False), sa.Column("thumbnail", sa.String(length=255), nullable=True), @@ -36,27 +29,37 @@ def upgrade(): sa.PrimaryKeyConstraint("id", name=op.f("pk_image")), ) op.create_table( - "permission", - sa.Column("name", sa.String(length=30), nullable=True), - sa.Column("id", flaschengeist.models.Serial(), nullable=False), - sa.PrimaryKeyConstraint("id", name=op.f("pk_permission")), - sa.UniqueConstraint("name", name=op.f("uq_permission_name")), + "plugin", + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), + sa.Column("name", sa.String(length=127), nullable=False), + sa.Column("version", sa.String(length=30), nullable=False), + sa.Column("enabled", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id", name=op.f("pk_plugin")), ) op.create_table( "role", - sa.Column("id", flaschengeist.models.Serial(), nullable=False), + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), sa.Column("name", sa.String(length=30), nullable=True), sa.PrimaryKeyConstraint("id", name=op.f("pk_role")), sa.UniqueConstraint("name", name=op.f("uq_role_name")), ) op.create_table( - "role_x_permission", - sa.Column("role_id", flaschengeist.models.Serial(), nullable=True), - sa.Column("permission_id", flaschengeist.models.Serial(), nullable=True), - sa.ForeignKeyConstraint( - ["permission_id"], ["permission.id"], name=op.f("fk_role_x_permission_permission_id_permission") - ), - sa.ForeignKeyConstraint(["role_id"], ["role.id"], name=op.f("fk_role_x_permission_role_id_role")), + "permission", + sa.Column("name", sa.String(length=30), nullable=True), + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), + sa.Column("plugin", flaschengeist.database.types.Serial(), nullable=True), + sa.ForeignKeyConstraint(["plugin"], ["plugin.id"], name=op.f("fk_permission_plugin_plugin")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_permission")), + sa.UniqueConstraint("name", name=op.f("uq_permission_name")), + ) + op.create_table( + "plugin_setting", + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), + sa.Column("plugin", flaschengeist.database.types.Serial(), nullable=True), + sa.Column("name", sa.String(length=127), nullable=False), + sa.Column("value", sa.PickleType(), nullable=True), + sa.ForeignKeyConstraint(["plugin"], ["plugin.id"], name=op.f("fk_plugin_setting_plugin_plugin")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_plugin_setting")), ) op.create_table( "user", @@ -67,48 +70,58 @@ def upgrade(): sa.Column("deleted", sa.Boolean(), nullable=True), sa.Column("birthday", sa.Date(), nullable=True), sa.Column("mail", sa.String(length=60), nullable=True), - sa.Column("id", flaschengeist.models.Serial(), nullable=False), - sa.Column("avatar", flaschengeist.models.Serial(), nullable=True), + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), + sa.Column("avatar", flaschengeist.database.types.Serial(), nullable=True), sa.ForeignKeyConstraint(["avatar"], ["image.id"], name=op.f("fk_user_avatar_image")), sa.PrimaryKeyConstraint("id", name=op.f("pk_user")), sa.UniqueConstraint("userid", name=op.f("uq_user_userid")), ) op.create_table( "notification", - sa.Column("id", flaschengeist.models.Serial(), nullable=False), - sa.Column("plugin", sa.String(length=127), nullable=False), + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), sa.Column("text", sa.Text(), nullable=True), sa.Column("data", sa.PickleType(), nullable=True), - sa.Column("time", flaschengeist.models.UtcDateTime(), nullable=False), - sa.Column("user_id", flaschengeist.models.Serial(), nullable=False), - sa.ForeignKeyConstraint(["user_id"], ["user.id"], name=op.f("fk_notification_user_id_user")), + sa.Column("time", flaschengeist.database.types.UtcDateTime(), nullable=False), + sa.Column("user", flaschengeist.database.types.Serial(), nullable=False), + sa.Column("plugin", flaschengeist.database.types.Serial(), nullable=False), + sa.ForeignKeyConstraint(["plugin"], ["plugin.id"], name=op.f("fk_notification_plugin_plugin")), + sa.ForeignKeyConstraint(["user"], ["user.id"], name=op.f("fk_notification_user_user")), sa.PrimaryKeyConstraint("id", name=op.f("pk_notification")), ) op.create_table( "password_reset", - sa.Column("user", flaschengeist.models.Serial(), nullable=False), + sa.Column("user", flaschengeist.database.types.Serial(), nullable=False), sa.Column("token", sa.String(length=32), nullable=True), - sa.Column("expires", flaschengeist.models.UtcDateTime(), nullable=True), + sa.Column("expires", flaschengeist.database.types.UtcDateTime(), nullable=True), sa.ForeignKeyConstraint(["user"], ["user.id"], name=op.f("fk_password_reset_user_user")), sa.PrimaryKeyConstraint("user", name=op.f("pk_password_reset")), ) + op.create_table( + "role_x_permission", + sa.Column("role_id", flaschengeist.database.types.Serial(), nullable=True), + sa.Column("permission_id", flaschengeist.database.types.Serial(), nullable=True), + sa.ForeignKeyConstraint( + ["permission_id"], ["permission.id"], name=op.f("fk_role_x_permission_permission_id_permission") + ), + sa.ForeignKeyConstraint(["role_id"], ["role.id"], name=op.f("fk_role_x_permission_role_id_role")), + ) op.create_table( "session", - sa.Column("expires", flaschengeist.models.UtcDateTime(), nullable=True), + sa.Column("expires", flaschengeist.database.types.UtcDateTime(), nullable=True), sa.Column("token", sa.String(length=32), nullable=True), sa.Column("lifetime", sa.Integer(), nullable=True), - sa.Column("browser", sa.String(length=30), nullable=True), - sa.Column("platform", sa.String(length=30), nullable=True), - sa.Column("id", flaschengeist.models.Serial(), nullable=False), - sa.Column("user_id", flaschengeist.models.Serial(), nullable=True), + sa.Column("browser", sa.String(length=127), nullable=True), + sa.Column("platform", sa.String(length=64), nullable=True), + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), + sa.Column("user_id", flaschengeist.database.types.Serial(), nullable=True), sa.ForeignKeyConstraint(["user_id"], ["user.id"], name=op.f("fk_session_user_id_user")), sa.PrimaryKeyConstraint("id", name=op.f("pk_session")), sa.UniqueConstraint("token", name=op.f("uq_session_token")), ) op.create_table( "user_attribute", - sa.Column("id", flaschengeist.models.Serial(), nullable=False), - sa.Column("user", flaschengeist.models.Serial(), nullable=False), + sa.Column("id", flaschengeist.database.types.Serial(), nullable=False), + sa.Column("user", flaschengeist.database.types.Serial(), nullable=False), sa.Column("name", sa.String(length=30), nullable=True), sa.Column("value", sa.PickleType(), nullable=True), sa.ForeignKeyConstraint(["user"], ["user.id"], name=op.f("fk_user_attribute_user_user")), @@ -116,8 +129,8 @@ def upgrade(): ) op.create_table( "user_x_role", - sa.Column("user_id", flaschengeist.models.Serial(), nullable=True), - sa.Column("role_id", flaschengeist.models.Serial(), nullable=True), + sa.Column("user_id", flaschengeist.database.types.Serial(), nullable=True), + sa.Column("role_id", flaschengeist.database.types.Serial(), nullable=True), sa.ForeignKeyConstraint(["role_id"], ["role.id"], name=op.f("fk_user_x_role_role_id_role")), sa.ForeignKeyConstraint(["user_id"], ["user.id"], name=op.f("fk_user_x_role_user_id_user")), ) @@ -129,11 +142,13 @@ def downgrade(): op.drop_table("user_x_role") op.drop_table("user_attribute") op.drop_table("session") + op.drop_table("role_x_permission") op.drop_table("password_reset") op.drop_table("notification") op.drop_table("user") - op.drop_table("role_x_permission") - op.drop_table("role") + op.drop_table("plugin_setting") op.drop_table("permission") + op.drop_table("role") + op.drop_table("plugin") op.drop_table("image") # ### end Alembic commands ### diff --git a/flaschengeist/app.py b/flaschengeist/app.py index ee8a6bd..d490965 100644 --- a/flaschengeist/app.py +++ b/flaschengeist/app.py @@ -4,7 +4,7 @@ from flask import Flask from flask_cors import CORS from datetime import datetime, date from flask.json import JSONEncoder, jsonify -from importlib.metadata import entry_points +from sqlalchemy.exc import OperationalError from werkzeug.exceptions import HTTPException from flaschengeist import logger @@ -37,21 +37,13 @@ class CustomJSONEncoder(JSONEncoder): @Hook("plugins.loaded") def load_plugins(app: Flask): app.config["FG_PLUGINS"] = {} - all_plugins = entry_points(group="flaschengeist.plugins") for plugin in pluginController.get_enabled_plugins(): logger.debug(f"Searching for enabled plugin {plugin.name}") - entry_point = all_plugins.select(name=plugin.name) - if not entry_point: - logger.error( - f"Plugin {plugin.name} was enabled, but could not be found.", - exc_info=True, - ) - continue try: - loaded = entry_point[0].load()(entry_point[0]) - if hasattr(plugin, "blueprint") and plugin.blueprint is not None: - app.register_blueprint(plugin.blueprint) + cls = plugin.entry_point.load() + if hasattr(cls, "blueprint") and cls.blueprint is not None: + app.register_blueprint(cls.blueprint) except: logger.error( f"Plugin {plugin.name} was enabled, but could not be loaded due to an error.", @@ -59,7 +51,8 @@ def load_plugins(app: Flask): ) continue logger.info(f"Loaded plugin: {plugin.name}") - app.config["FG_PLUGINS"][plugin.name] = loaded + app.config["FG_PLUGINS"][plugin.name] = cls.query.get(plugin.id) if plugin.id is not None else plugin + app.config["FG_PLUGINS"][plugin.name].load() def create_app(test_config=None, cli=False): @@ -79,7 +72,7 @@ def create_app(test_config=None, cli=False): def __get_state(): from . import __version__ as version - return jsonify({"plugins": app.config["FG_PLUGINS"], "version": version}) + return jsonify({"plugins": pluginController.get_loaded_plugins(), "version": version}) @app.errorhandler(Exception) def handle_exception(e): diff --git a/flaschengeist/cli/install_cmd.py b/flaschengeist/cli/install_cmd.py index e566163..e1e9244 100644 --- a/flaschengeist/cli/install_cmd.py +++ b/flaschengeist/cli/install_cmd.py @@ -4,7 +4,7 @@ from flask.cli import with_appcontext from flask_migrate import upgrade from flaschengeist.alembic import alembic_migrations_path -from flaschengeist.cli.plugin_cmd import install_plugin_command +from flaschengeist.controller import pluginController from flaschengeist.utils.hook import Hook @@ -12,9 +12,13 @@ from flaschengeist.utils.hook import Hook @with_appcontext @pass_context @Hook("plugins.installed") -def install(ctx): +def install(ctx: click.Context): + plugins = pluginController.get_enabled_plugins() + # Install database upgrade(alembic_migrations_path, revision="heads") # Install plugins - install_plugin_command(ctx, [], True) + for plugin in plugins: + plugin = pluginController.install_plugin(plugin.name) + pluginController.enable_plugin(plugin.id) diff --git a/flaschengeist/cli/plugin_cmd.py b/flaschengeist/cli/plugin_cmd.py index d8b90c4..5356eac 100644 --- a/flaschengeist/cli/plugin_cmd.py +++ b/flaschengeist/cli/plugin_cmd.py @@ -1,12 +1,13 @@ +import traceback import click from click.decorators import pass_context from flask import current_app from flask.cli import with_appcontext from importlib.metadata import EntryPoint, entry_points -from flaschengeist.database import db -from flaschengeist.config import config -from flaschengeist.models import Permission +from flaschengeist import logger +from flaschengeist.controller import pluginController +from werkzeug.exceptions import NotFound @click.group() @@ -14,33 +15,34 @@ def plugin(): pass -def install_plugin_command(ctx, plugin, all): - """Install one or more plugins""" - if not all and len(plugin) == 0: - ctx.fail("At least one plugin must be specified, or use `--all` flag.") - - if all: - plugins = current_app.config["FG_PLUGINS"] - else: +@plugin.command() +@click.argument("plugin", nargs=-1, required=True, type=str) +@with_appcontext +@pass_context +def enable(ctx, plugin): + """Enable one or more plugins""" + for name in plugin: + click.echo(f"Enabling {name}{'.'*(20-len(name))}", nl=False) try: - plugins = {plugin_name: current_app.config["FG_PLUGINS"][plugin_name] for plugin_name in plugin} - except KeyError as e: - ctx.fail(f"Invalid plugin name, could not find >{e.args[0]}<") + pluginController.enable_plugin(name) + click.secho(" ok", fg="green") + except NotFound: + click.secho(" not installed / not found", fg="red") - for name, plugin in plugins.items(): - click.echo(f"Installing {name}{'.'*(20-len(name))}", nl=False) - # Install permissions - if plugin.permissions: - cur_perm = set(x.name for x in Permission.query.filter(Permission.name.in_(plugin.permissions)).all()) - all_perm = set(plugin.permissions) - add = all_perm - cur_perm - if add: - db.session.bulk_save_objects([Permission(name=x) for x in all_perm]) - db.session.commit() - # Custom installation steps - plugin.install() - click.secho(" ok", fg="green") +@plugin.command() +@click.argument("plugin", nargs=-1, required=True, type=str) +@with_appcontext +@pass_context +def disable(ctx, plugin): + """Disable one or more plugins""" + for name in plugin: + click.echo(f"Disabling {name}{'.'*(20-len(name))}", nl=False) + try: + pluginController.disable_plugin(name) + click.secho(" ok", fg="green") + except NotFound: + click.secho(" not installed / not found", fg="red") @plugin.command() @@ -48,42 +50,62 @@ def install_plugin_command(ctx, plugin, all): @click.option("--all", help="Install all enabled plugins", is_flag=True) @with_appcontext @pass_context -def install(ctx, plugin, all): +def install(ctx: click.Context, plugin, all): """Install one or more plugins""" - return install_plugin_command(ctx, plugin, all) + all_plugins = entry_points(group="flaschengeist.plugins") + + if all: + plugins = [ep.name for ep in all_plugins] + elif len(plugin) > 0: + plugins = plugin + for name in plugin: + if not all_plugins.select(name=name): + ctx.fail(f"Invalid plugin name, could not find >{name}<") + else: + ctx.fail("At least one plugin must be specified, or use `--all` flag.") + + 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") @plugin.command() -@click.argument("plugin", nargs=-1, type=str) +@click.argument("plugin", nargs=-1, required=True, type=str) @with_appcontext @pass_context def uninstall(ctx: click.Context, plugin): """Uninstall one or more plugins""" - if len(plugin) == 0: - ctx.fail("At least one plugin must be specified") - + plugins = {plg.name: plg for plg in pluginController.get_installed_plugins() if plg.name in plugin} try: - plugins = {plugin_name: current_app.config["FG_PLUGINS"][plugin_name] for plugin_name in plugin} - except KeyError as e: - ctx.fail(f"Invalid plugin ID, could not find >{e.args[0]}<") - - if ( - click.prompt( - "You are going to uninstall:\n\n" - f"\t{', '.join([plugin_name for plugin_name in plugins.keys()])}\n\n" - "Are you sure?", - default="n", - show_choices=True, - type=click.Choice(["y", "N"], False), - ).lower() - != "y" - ): - ctx.exit() - for name, plugin in plugins.items(): - click.echo(f"Uninstalling {name}{'.'*(20-len(name))}", nl=False) - plugin.uninstall() - click.secho(" ok", fg="green") + for name in plugin: + pluginController.disable_plugin(plugins[name]) + if ( + click.prompt( + "You are going to uninstall:\n\n" + f"\t{', '.join([plugin_name for plugin_name in plugins.keys()])}\n\n" + "Are you sure?", + default="n", + show_choices=True, + type=click.Choice(["y", "N"], False), + ).lower() + != "y" + ): + ctx.exit() + click.echo(f"Uninstalling {name}{'.'*(20-len(name))}", nl=False) + pluginController.uninstall_plugin(plugins[name]) + click.secho(" ok", fg="green") + except KeyError: + ctx.fail(f"Invalid plugin ID, could not find >{name}<") @plugin.command() @@ -97,21 +119,27 @@ def ls(enabled, no_header): return p.version plugins = entry_points(group="flaschengeist.plugins") - enabled_plugins = [key for key, value in config.items() if "enabled" in value and value["enabled"]] + [ - config["FLASCHENGEIST"]["auth"] - ] + installed_plugins = {plg.name: plg for plg in pluginController.get_installed_plugins()} loaded_plugins = current_app.config["FG_PLUGINS"].keys() if not no_header: - print(f"{' '*13}{'name': <20}|{'version': >10}") - print("-" * 46) + print(f"{' '*13}{'name': <20}| version | {' ' * 8} state") + print("-" * 63) for plugin in plugins: - if enabled and plugin.name not in enabled_plugins: + is_installed = plugin.name in installed_plugins.keys() + is_enabled = is_installed and installed_plugins[plugin.name].enabled + if enabled and is_enabled: continue - print( - f"{plugin.name: <33}|{plugin_version(plugin): >12}" - f"{click.style(' (enabled)', fg='green') if plugin.name in enabled_plugins else ' (disabled)'}" - ) - - for plugin in [value for value in enabled_plugins if value not in loaded_plugins]: - print(f"{plugin: <33}|{' '*12}" f"{click.style(' (not found)', fg='red')}") + print(f"{plugin.name: <33}|{plugin_version(plugin): >12} | ", end="") + if is_enabled: + if plugin.name in loaded_plugins: + print(click.style(" enabled", fg="green")) + else: + print(click.style("(failed to load)", fg="red")) + elif is_installed: + print(click.style(" disabled", fg="yellow")) + else: + print("not installed") + for name, plugin in installed_plugins.items(): + if plugin.enabled and name not in loaded_plugins: + print(f"{name: <33}|{'': >12} |" f"{click.style(' failed to load', fg='red')}") diff --git a/flaschengeist/controller/pluginController.py b/flaschengeist/controller/pluginController.py index 86ea301..7cccc4e 100644 --- a/flaschengeist/controller/pluginController.py +++ b/flaschengeist/controller/pluginController.py @@ -3,84 +3,58 @@ Used by plugins for setting and notification functionality. """ -import sqlalchemy - from typing import Union from flask import current_app -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import NotFound, BadRequest from sqlalchemy.exc import OperationalError from importlib.metadata import entry_points +from flaschengeist import version as flaschengeist_version + from .. import logger from ..database import db from ..utils.hook import Hook -from ..models import Plugin, PluginSetting, Notification +from ..plugins import Plugin, AuthPlugin +from ..models import Notification -def get_enabled_plugins(): +__required_plugins = ["users", "roles", "scheduler", "auth"] + + +def get_authentication_provider(): + return [plugin for plugin in get_loaded_plugins().values() if isinstance(plugin, AuthPlugin)] + + +def get_loaded_plugins(plugin_name: str = None): + """Get loaded plugin(s)""" + plugins = current_app.config["FG_PLUGINS"] + if plugin_name is not None: + plugins = [plugins[plugin_name]] + return {name: db.session.merge(plugins[name], load=False) for name in plugins} + + +def get_installed_plugins() -> list[Plugin]: + """Get all installed plugins""" + return Plugin.query.all() + + +def get_enabled_plugins() -> list[Plugin]: + """Get all installed and enabled plugins""" try: enabled_plugins = Plugin.query.filter(Plugin.enabled == True).all() except OperationalError as e: - - class PluginStub: - def __init__(self, name) -> None: - self.name = name - self.version = "?" - logger.error("Could not connect to database or database not initialized! No plugins enabled!") logger.debug("Can not query enabled plugins", exc_info=True) + # Fake load required plugins so the database can at least be installed enabled_plugins = [ - PluginStub("auth"), - PluginStub("roles"), - PluginStub("users"), - PluginStub("scheduler"), + entry_points(group="flaschengeist.plugins", name=name)[0].load()( + name=name, enabled=True, installed_version=flaschengeist_version + ) + for name in __required_plugins ] return enabled_plugins -def get_setting(plugin_id: str, name: str, **kwargs): - """Get plugin setting from database - - Args: - plugin_id: ID of the plugin - name: string identifying the setting - default: Default value - Returns: - Value stored in database (native python) - Raises: - `KeyError` if no such setting exists in the database - """ - try: - setting = PluginSetting.query.filter(PluginSetting.plugin == plugin_id).filter(PluginSetting.name == name).one() - return setting.value - except sqlalchemy.orm.exc.NoResultFound: - if "default" in kwargs: - return kwargs["default"] - else: - raise KeyError - - -def set_setting(plugin_id: str, name: str, value): - """Save setting in database - - Args: - plugin_id: ID of the plugin - name: String identifying the setting - value: Value to be stored - """ - setting = ( - PluginSetting.query.filter(PluginSetting.plugin == plugin_id).filter(PluginSetting.name == name).one_or_none() - ) - if setting is not None: - if value is None: - db.session.delete(setting) - else: - setting.value = value - else: - db.session.add(PluginSetting(plugin=plugin_id, name=name, value=value)) - db.session.commit() - - def notify(plugin_id: str, user, text: str, data=None): """Create a new notification for an user @@ -108,55 +82,67 @@ def install_plugin(plugin_name: str): if not entry_point: raise NotFound - plugin = entry_point[0].load()(entry_point[0]) - entity = Plugin(name=plugin.name, version=plugin.version) - db.session.add(entity) + cls = entry_point[0].load() + plugin = cls.query.filter(Plugin.name == plugin_name).one_or_none() + if plugin is None: + plugin = cls(name=plugin_name, installed_version=entry_point[0].dist.version) + db.session.add(plugin) + db.session.flush() + # Custom installation steps + plugin.install() db.session.commit() - return entity + + return plugin @Hook("plugin.uninstalled") -def uninstall_plugin(plugin_id: Union[str, int]): +def uninstall_plugin(plugin_id: Union[str, int, Plugin]): plugin = disable_plugin(plugin_id) logger.debug(f"Uninstall plugin {plugin.name}") - - entity = current_app.config["FG_PLUGINS"][plugin.name] - entity.uninstall() - del current_app.config["FG_PLUGINS"][plugin.name] + plugin.uninstall() db.session.delete(plugin) db.session.commit() @Hook("plugins.enabled") -def enable_plugin(plugin_id: Union[str, int]): +def enable_plugin(plugin_id: Union[str, int]) -> Plugin: logger.debug(f"Enabling plugin {plugin_id}") - plugin: Plugin = Plugin.query + plugin = Plugin.query if isinstance(plugin_id, str): plugin = plugin.filter(Plugin.name == plugin_id).one_or_none() - if plugin is None: - logger.debug("Plugin not installed, trying to install") - plugin = install_plugin(plugin_id) - else: + elif isinstance(plugin_id, int): plugin = plugin.get(plugin_id) - if plugin is None: - raise NotFound + else: + raise TypeError + if plugin is None: + raise NotFound plugin.enabled = True db.session.commit() - + plugin = plugin.entry_point.load().query.get(plugin.id) + current_app.config["FG_PLUGINS"][plugin.name] = plugin return plugin @Hook("plugins.disabled") -def disable_plugin(plugin_id: Union[str, int]): +def disable_plugin(plugin_id: Union[str, int, Plugin]): logger.debug(f"Disabling plugin {plugin_id}") plugin: Plugin = Plugin.query if isinstance(plugin_id, str): plugin = plugin.filter(Plugin.name == plugin_id).one_or_none() - else: + elif isinstance(plugin_id, int): plugin = plugin.get(plugin_id) + elif isinstance(plugin_id, Plugin): + plugin = plugin_id + else: + raise TypeError if plugin is None: raise NotFound + if plugin.name in __required_plugins: + raise BadRequest plugin.enabled = False db.session.commit() + if plugin.name in current_app.config["FG_PLUGINS"].keys(): + del current_app.config["FG_PLUGINS"][plugin.name] + return plugin diff --git a/flaschengeist/controller/userController.py b/flaschengeist/controller/userController.py index 610db39..520ec31 100644 --- a/flaschengeist/controller/userController.py +++ b/flaschengeist/controller/userController.py @@ -3,7 +3,6 @@ import secrets from io import BytesIO from sqlalchemy import exc -from flask import current_app from datetime import datetime, timedelta, timezone from flask.helpers import send_file from werkzeug.exceptions import NotFound, BadRequest, Forbidden @@ -16,7 +15,8 @@ from ..models.user import _PasswordReset from ..utils.hook import Hook from ..utils.datetime import from_iso_format from ..utils.foreign_keys import merge_references -from ..controller import imageController, messageController, sessionController +from ..controller import imageController, messageController, pluginController, sessionController +from ..plugins import AuthPlugin def __active_users(): @@ -41,17 +41,33 @@ def _generate_password_reset(user): return reset +def get_provider(userid: str): + return [p for p in pluginController.get_authentication_provider() if p.user_exists(userid)][0] + + +@Hook +def update_user(user: User, backend: AuthPlugin): + """Update user data from backend + + This is seperate function to provide a hook""" + backend.update_user(user) + if not user.display_name: + user.display_name = "{} {}.".format(user.firstname, user.lastname[0]) + db.session.commit() + + def login_user(username, password): logger.info("login user {{ {} }}".format(username)) - - user = find_user(username) - if not user: - logger.debug("User not found in Database.") - user = User(userid=username) - db.session.add(user) - if current_app.config["FG_AUTH_BACKEND"].login(user, password): - update_user(user) - return user + for provider in pluginController.get_authentication_provider(): + uid = provider.login(username, password) + if isinstance(uid, str): + user = get_user(uid) + if not user: + logger.debug("User not found in Database.") + user = User(userid=uid) + db.session.add(user) + update_user(user, provider) + return user return None @@ -84,14 +100,6 @@ def reset_password(token: str, password: str): db.session.commit() -@Hook -def update_user(user): - current_app.config["FG_AUTH_BACKEND"].update_user(user) - if not user.display_name: - user.display_name = "{} {}.".format(user.firstname, user.lastname[0]) - db.session.commit() - - def set_roles(user: User, roles: list[str], create=False): """Set roles of user @@ -115,7 +123,7 @@ def set_roles(user: User, roles: list[str], create=False): user.roles_ = fetched -def modify_user(user, password, new_password=None): +def modify_user(user: User, password: str, new_password: str = None): """Modify given user on the backend Args: @@ -127,7 +135,8 @@ def modify_user(user, password, new_password=None): NotImplemented: If backend is not capable of this operation BadRequest: Password is wrong or other logic issues """ - current_app.config["FG_AUTH_BACKEND"].modify_user(user, password, new_password) + provider = get_provider(user.userid) + provider.modify_user(user, password, new_password) if new_password: logger.debug(f"Password changed for user {user.userid}") @@ -165,37 +174,13 @@ def get_user(uid, deleted=False) -> User: return user -def find_user(uid_mail): - """Finding an user by userid or mail in database or auth-backend - Args: - uid_mail: userid and or mail to search for - Returns: - User if found or None - """ - mail = uid_mail.split("@") - mail = len(mail) == 2 and len(mail[0]) > 0 and len(mail[1]) > 0 - - query = User.userid == uid_mail - if mail: - query |= User.mail == uid_mail - user = User.query.filter(query).one_or_none() - if user: - update_user(user) - else: - user = current_app.config["FG_AUTH_BACKEND"].find_user(uid_mail, uid_mail if mail else None) - if user: - if not user.display_name: - user.display_name = "{} {}.".format(user.firstname, user.lastname[0]) - db.session.add(user) - db.session.commit() - return user - - @Hook def delete_user(user: User): """Delete given user""" # First let the backend delete the user, as this might fail - current_app.config["FG_AUTH_BACKEND"].delete_user(user) + provider = get_provider(user.userid) + provider.delete_user(user) + # Clear all easy relationships user.avatar_ = None user._attributes.clear() @@ -247,10 +232,14 @@ def register(data, passwd=None): set_roles(user, roles) password = passwd if passwd else secrets.token_urlsafe(16) - current_app.config["FG_AUTH_BACKEND"].create_user(user, password) try: + provider = [p for p in pluginController.get_authentication_provider() if p.can_register()][0] + provider.create_user(user, password) db.session.add(user) db.session.commit() + except IndexError: + logger.error("No authentication backend, allowing registering new users, found.") + raise BadRequest except exc.IntegrityError: raise BadRequest("userid already in use") @@ -265,7 +254,7 @@ def register(data, passwd=None): ) messageController.send_message(messageController.Message(user, text, subject)) - find_user(user.userid) + provider.update_user(user) return user @@ -274,19 +263,20 @@ def load_avatar(user: User): if user.avatar_ is not None: return imageController.send_image(image=user.avatar_) else: - avatar = current_app.config["FG_AUTH_BACKEND"].get_avatar(user) + provider = get_provider(user.userid) + avatar = provider.get_avatar(user) if len(avatar.binary) > 0: return send_file(BytesIO(avatar.binary), avatar.mimetype) raise NotFound def save_avatar(user, file): - current_app.config["FG_AUTH_BACKEND"].set_avatar(user, file) + get_provider(user.userid).set_avatar(user, file) db.session.commit() def delete_avatar(user): - current_app.config["FG_AUTH_BACKEND"].delete_avatar(user) + get_provider(user.userid).delete_avatar(user) db.session.commit() diff --git a/flaschengeist/database/types.py b/flaschengeist/database/types.py index 369bb30..645ecdd 100644 --- a/flaschengeist/database/types.py +++ b/flaschengeist/database/types.py @@ -1,4 +1,4 @@ -import sys +from importlib import import_module import datetime from sqlalchemy import BigInteger, util @@ -12,12 +12,11 @@ class ModelSerializeMixin: """ def __is_optional(self, param): - if sys.version_info < (3, 8): - return False - import typing - hint = typing.get_type_hints(self.__class__)[param] + module = import_module("flaschengeist.models").__dict__ + + hint = typing.get_type_hints(self.__class__, globalns=module)[param] if ( typing.get_origin(hint) is typing.Union and len(typing.get_args(hint)) == 2 diff --git a/flaschengeist/models/plugin.py b/flaschengeist/models/plugin.py index eb5cf35..a912afd 100644 --- a/flaschengeist/models/plugin.py +++ b/flaschengeist/models/plugin.py @@ -1,26 +1,72 @@ from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered) from typing import Any +from sqlalchemy.orm.collections import attribute_mapped_collection from ..database import db from ..database.types import Serial -class Plugin(db.Model): - __tablename__ = "plugin" - id: int = db.Column("id", Serial, primary_key=True) - name: str = db.Column(db.String(127), nullable=False) - version: str = db.Column(db.String(30), nullable=False) - """The latest installed version""" - enabled: bool = db.Column(db.Boolean, default=False) - - settings_ = db.relationship("PluginSetting", cascade="all, delete") - permissions_ = db.relationship("Permission", cascade="all, delete") - - class PluginSetting(db.Model): __tablename__ = "plugin_setting" id = db.Column("id", Serial, primary_key=True) plugin_id: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id")) name: str = db.Column(db.String(127), nullable=False) value: Any = db.Column(db.PickleType(protocol=4)) + + +class BasePlugin(db.Model): + __tablename__ = "plugin" + id: int = db.Column("id", Serial, primary_key=True) + name: str = db.Column(db.String(127), nullable=False) + """Name of the plugin, loaded from distribution""" + installed_version: str = db.Column("version", db.String(30), nullable=False) + """The latest installed version""" + enabled: bool = db.Column(db.Boolean, default=False) + """Enabled state of the plugin""" + permissions: list = db.relationship( + "Permission", cascade="all, delete, delete-orphan", back_populates="plugin_", lazy="select" + ) + """Optional list of custom permissions used by the plugin + + A good style is to name the permissions with a prefix related to the plugin name, + to prevent clashes with other plugins. E. g. instead of *delete* use *plugin_delete*. + """ + + __settings: dict[str, "PluginSetting"] = db.relationship( + "PluginSetting", + collection_class=attribute_mapped_collection("name"), + cascade="all, delete, delete-orphan", + lazy="select", + ) + + def get_setting(self, name: str, **kwargs): + """Get plugin setting + + Args: + name: string identifying the setting + default: Default value + Returns: + Value stored in database (native python) + Raises: + `KeyError` if no such setting exists in the database + """ + try: + return self.__settings[name].value + except KeyError as e: + if "default" in kwargs: + return kwargs["default"] + raise e + + def set_setting(self, name: str, value): + """Save setting in database + + Args: + name: String identifying the setting + value: Value to be stored + """ + if value is None and name in self.__settings.keys(): + del self.__settings[name] + else: + setting = self.__settings.setdefault(name, PluginSetting(plugin_id=self.id, name=name, value=None)) + setting.value = value diff --git a/flaschengeist/models/user.py b/flaschengeist/models/user.py index 077b78c..e12db4a 100644 --- a/flaschengeist/models/user.py +++ b/flaschengeist/models/user.py @@ -24,8 +24,9 @@ class Permission(db.Model, ModelSerializeMixin): __tablename__ = "permission" name: str = db.Column(db.String(30), unique=True) - _id = db.Column("id", Serial, primary_key=True) - _plugin_id: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id")) + id_ = db.Column("id", Serial, primary_key=True) + plugin_id_: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id")) + plugin_ = db.relationship("Plugin", lazy="select", back_populates="permissions", enable_typechecks=False) class Role(db.Model, ModelSerializeMixin): @@ -64,9 +65,7 @@ class User(db.Model, ModelSerializeMixin): # Protected stuff for backend use only id_ = db.Column("id", Serial, primary_key=True) roles_: list[Role] = db.relationship("Role", secondary=association_table, cascade="save-update, merge") - sessions_: list[Session] = db.relationship( - "Session", back_populates="user_", cascade="all, delete, delete-orphan" - ) + sessions_: list[Session] = db.relationship("Session", back_populates="user_", cascade="all, delete, delete-orphan") avatar_: Optional[Image] = db.relationship("Image", cascade="all, delete, delete-orphan", single_parent=True) reset_requests_: list["_PasswordReset"] = db.relationship("_PasswordReset", cascade="all, delete, delete-orphan") diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index f1a68a0..ff53f86 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -4,13 +4,13 @@ """ -from typing import Optional -from importlib.metadata import Distribution, EntryPoint -from werkzeug.exceptions import MethodNotAllowed, NotFound +from typing import Union +from importlib.metadata import entry_points +from werkzeug.exceptions import NotFound from werkzeug.datastructures import FileStorage -from flaschengeist.models import User -from flaschengeist.models.user import _Avatar +from flaschengeist.models.plugin import BasePlugin +from flaschengeist.models.user import _Avatar, Permission from flaschengeist.utils.hook import HookBefore, HookAfter __all__ = [ @@ -20,7 +20,7 @@ __all__ = [ "before_role_updated", "before_update_user", "after_role_updated", - "BasePlugin", + "Plugin", "AuthPlugin", ] @@ -71,7 +71,7 @@ Passed args: """ -class BasePlugin: +class Plugin(BasePlugin): """Base class for all Plugins All plugins must derived from this class. @@ -82,47 +82,30 @@ class BasePlugin: - *models*: Your models, used for API export """ - name: str - """Name of the plugin, loaded from EntryPoint""" - - version: str - """Version of the plugin, loaded from Distribution""" - - dist: Distribution - """Distribution of this plugin""" - blueprint = None """Optional `flask.blueprint` if the plugin uses custom routes""" - permissions: list[str] = [] - """Optional list of custom permissions used by the plugin - - A good style is to name the permissions with a prefix related to the plugin name, - to prevent clashes with other plugins. E. g. instead of *delete* use *plugin_delete*. - """ - models = None """Optional module containing the SQLAlchemy models used by the plugin""" - migrations: Optional[tuple[str, str]] = None - """Optional identifiers of the migration versions - - If custom database tables are used migrations must be provided and the - head and removal versions had to be defined, e.g. - - ``` - migrations = ("head_hash", "removal_hash") - ``` - """ + @property + def version(self) -> str: + """Version of the plugin, loaded from Distribution""" + return self.dist.version - def __init__(self, entry_point: EntryPoint): - """Constructor called by create_app - Args: - entry_point: EntryPoint from which this plugin was loaded - """ - self.version = entry_point.dist.version - self.name = entry_point.name - self.dist = entry_point.dist + @property + def dist(self): + """Distribution of this plugin""" + return self.entry_point.dist + + @property + def entry_point(self): + ep = entry_points(group="flaschengeist.plugins", name=self.name) + return ep[0] + + def load(self): + """__init__ like function that is called when the plugin is initially loaded""" + pass def install(self): """Installation routine @@ -145,40 +128,6 @@ class BasePlugin: """ pass - @property - def installed_version(self): - """Installed version of the plugin""" - from ..controller import pluginController - - self.__installed_version = pluginController.get_installed_version(self.name) - return self.__installed_version - - def get_setting(self, name: str, **kwargs): - """Get plugin setting from database - - Args: - name: string identifying the setting - default: Default value - Returns: - Value stored in database (native python) - Raises: - `KeyError` if no such setting exists in the database - """ - from ..controller import pluginController - - return pluginController.get_setting(self.name, name, **kwargs) - - def set_setting(self, name: str, value): - """Save setting in database - - Args: - name: String identifying the setting - value: Value to be stored - """ - from ..controller import pluginController - - return pluginController.set_setting(self.name, name, value) - def notify(self, user, text: str, data=None): """Create a new notification for an user @@ -203,40 +152,53 @@ class BasePlugin: """ return {"version": self.version, "permissions": self.permissions} + def install_permissions(self, permissions: list[str]): + """Helper for installing a list of strings as permissions -class AuthPlugin(BasePlugin): + Args: + permissions: List of permissions to install + """ + cur_perm = set(x.name for x in self.permissions) + all_perm = set(permissions) + + new_perms = all_perm - cur_perm + self.permissions = list(filter(lambda x: x.name in permissions, self.permissions)) + [ + Permission(name=x, plugin_=self) for x in new_perms + ] + + +class AuthPlugin(Plugin): """Base class for all authentification plugins - See also `BasePlugin` + See also `Plugin` """ - def login(self, user, pw): + def login(self, login_name, password) -> Union[bool, str]: """Login routine, MUST BE IMPLEMENTED! Args: - user: User class containing at least the uid - pw: given password + login_name: The name the user entered + password: The password the user used to log in Returns: - Must return False if not found or invalid credentials, True if success + Must return False if not found or invalid credentials, otherwise the UID is returned """ raise NotImplemented - def update_user(self, user): + def update_user(self, user: "User"): """If backend is using external data, then update this user instance with external data Args: user: User object """ pass - def find_user(self, userid, mail=None): - """Find an user by userid or mail + def user_exists(self, userid) -> bool: + """Check if user exists on this backend Args: userid: Userid to search - mail: If set, mail to search Returns: - None or User + True or False """ - return None + raise NotImplemented def modify_user(self, user, password, new_password=None): """If backend is using (writeable) external data, then update the external database with the user provided. @@ -247,11 +209,14 @@ class AuthPlugin(BasePlugin): password: Password (some backends need the current password for changes) if None force edit (admin) new_password: If set a password change is requested Raises: - NotImplemented: If backend does not support this feature (or no password change) BadRequest: Logic error, e.g. password is wrong. Error: Other errors if backend went mad (are not handled and will result in a 500 error) """ - raise NotImplemented + pass + + def can_register(self): + """Check if this backend allows to register new users""" + return False def create_user(self, user, password): """If backend is using (writeable) external data, then create a new user on the external database. @@ -272,7 +237,7 @@ class AuthPlugin(BasePlugin): """ raise MethodNotAllowed - def get_avatar(self, user): + def get_avatar(self, user) -> _Avatar: """Retrieve avatar for given user (if supported by auth backend) Default behavior is to use native Image objects, diff --git a/flaschengeist/plugins/auth/__init__.py b/flaschengeist/plugins/auth/__init__.py index be20ac2..7f06302 100644 --- a/flaschengeist/plugins/auth/__init__.py +++ b/flaschengeist/plugins/auth/__init__.py @@ -6,13 +6,13 @@ from flask import Blueprint, request, jsonify from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized from flaschengeist import logger -from flaschengeist.plugins import BasePlugin +from flaschengeist.plugins import Plugin from flaschengeist.utils.HTTP import no_content, created from flaschengeist.utils.decorators import login_required from flaschengeist.controller import sessionController, userController -class AuthRoutePlugin(BasePlugin): +class AuthRoutePlugin(Plugin): blueprint = Blueprint("auth", __name__) diff --git a/flaschengeist/plugins/auth_plain/__init__.py b/flaschengeist/plugins/auth_plain/__init__.py index 50ab4af..bdf01ac 100644 --- a/flaschengeist/plugins/auth_plain/__init__.py +++ b/flaschengeist/plugins/auth_plain/__init__.py @@ -7,42 +7,25 @@ import os import hashlib import binascii from werkzeug.exceptions import BadRequest -from flaschengeist.plugins import AuthPlugin, plugins_installed +from flaschengeist.plugins import AuthPlugin from flaschengeist.models import User, Role, Permission from flaschengeist.database import db from flaschengeist import logger class AuthPlain(AuthPlugin): - def install(self): - plugins_installed(self.post_install) + def can_register(self): + return True - def post_install(self, *args, **kwargs): - if User.query.filter(User.deleted == False).count() == 0: - logger.info("Installing admin user") - role = Role.query.filter(Role.name == "Superuser").first() - if role is None: - role = Role(name="Superuser", permissions=Permission.query.all()) - admin = User( - userid="admin", - firstname="Admin", - lastname="Admin", - mail="", - roles_=[role], - ) - self.modify_user(admin, None, "admin") - db.session.add(admin) - db.session.commit() - logger.warning( - "New administrator user was added, please change the password or remove it before going into" - "production mode. Initial credentials:\n" - "name: admin\n" - "password: admin" - ) - - def login(self, user: User, password: str): - if user.has_attribute("password"): - return AuthPlain._verify_password(user.get_attribute("password"), password) + def login(self, login_name, password): + users: list[User] = ( + User.query.filter((User.userid == login_name) | (User.mail == login_name)) + .filter(User._attributes.any(name="password")) + .all() + ) + for user in users: + if AuthPlain._verify_password(user.get_attribute("password"), password): + return user.userid return False def modify_user(self, user, password, new_password=None): @@ -51,6 +34,12 @@ class AuthPlain(AuthPlugin): if new_password: user.set_attribute("password", AuthPlain._hash_password(new_password)) + def user_exists(self, userid) -> bool: + return ( + db.session.query(User.id_).filter(User.userid == userid, User._attributes.any(name="password")).first() + is not None + ) + def create_user(self, user, password): if not user.userid: raise BadRequest("userid is missing for new user") @@ -68,7 +57,7 @@ class AuthPlain(AuthPlugin): return (salt + pass_hash).decode("ascii") @staticmethod - def _verify_password(stored_password, provided_password): + def _verify_password(stored_password: str, provided_password: str): salt = stored_password[:64] stored_password = stored_password[64:] pass_hash = hashlib.pbkdf2_hmac("sha3-512", provided_password.encode("utf-8"), salt.encode("ascii"), 100000) diff --git a/flaschengeist/plugins/message_mail.py b/flaschengeist/plugins/message_mail.py index 59d82d1..acc00de 100644 --- a/flaschengeist/plugins/message_mail.py +++ b/flaschengeist/plugins/message_mail.py @@ -4,13 +4,13 @@ from email.mime.multipart import MIMEMultipart from flaschengeist import logger from flaschengeist.models import User -from flaschengeist.plugins import BasePlugin +from flaschengeist.plugins import Plugin from flaschengeist.utils.hook import HookAfter from flaschengeist.controller import userController from flaschengeist.controller.messageController import Message -class MailMessagePlugin(BasePlugin): +class MailMessagePlugin(Plugin): def __init__(self, entry_point, config): super().__init__(entry_point, config) self.server = config["SERVER"] diff --git a/flaschengeist/plugins/roles/__init__.py b/flaschengeist/plugins/roles/__init__.py index cd2fae4..07380cb 100644 --- a/flaschengeist/plugins/roles/__init__.py +++ b/flaschengeist/plugins/roles/__init__.py @@ -6,7 +6,7 @@ Provides routes used to configure roles and permissions of users / roles. from werkzeug.exceptions import BadRequest from flask import Blueprint, request, jsonify -from flaschengeist.plugins import BasePlugin +from flaschengeist.plugins import Plugin from flaschengeist.controller import roleController from flaschengeist.utils.HTTP import created, no_content from flaschengeist.utils.decorators import login_required @@ -14,9 +14,11 @@ from flaschengeist.utils.decorators import login_required from . import permissions -class RolesPlugin(BasePlugin): +class RolesPlugin(Plugin): blueprint = Blueprint("roles", __name__) - permissions = permissions.permissions + + def install(self): + self.install_permissions(permissions.permissions) @RolesPlugin.blueprint.route("/roles", methods=["GET"]) diff --git a/flaschengeist/plugins/scheduler.py b/flaschengeist/plugins/scheduler.py index 43a0a8b..268bafd 100644 --- a/flaschengeist/plugins/scheduler.py +++ b/flaschengeist/plugins/scheduler.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from flaschengeist import logger from flaschengeist.config import config -from flaschengeist.plugins import BasePlugin +from flaschengeist.plugins import Plugin from flaschengeist.utils.HTTP import no_content @@ -38,11 +38,10 @@ def scheduled(id: str, replace=False, **kwargs): return real_decorator -class SchedulerPlugin(BasePlugin): - def __init__(self, entry_point): - super().__init__(entry_point) - self.blueprint = Blueprint(self.name, __name__) +class SchedulerPlugin(Plugin): + blueprint = Blueprint("scheduler", __name__) + def load(self): def __view_func(): self.run_tasks() return no_content() diff --git a/flaschengeist/plugins/users/__init__.py b/flaschengeist/plugins/users/__init__.py index 7511a3f..e819486 100644 --- a/flaschengeist/plugins/users/__init__.py +++ b/flaschengeist/plugins/users/__init__.py @@ -9,7 +9,7 @@ from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed from . import permissions from flaschengeist import logger from flaschengeist.config import config -from flaschengeist.plugins import BasePlugin +from flaschengeist.plugins import Plugin from flaschengeist.models import User from flaschengeist.utils.decorators import login_required, extract_session, headers from flaschengeist.controller import userController @@ -17,9 +17,11 @@ from flaschengeist.utils.HTTP import created, no_content from flaschengeist.utils.datetime import from_iso_format -class UsersPlugin(BasePlugin): +class UsersPlugin(Plugin): blueprint = Blueprint("users", __name__) - permissions = permissions.permissions + + def install(self): + self.install_permissions(permissions.permissions) @UsersPlugin.blueprint.route("/users", methods=["POST"])