From d3530cc15f71688893fcc7c502143e7b5d6534b8 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 18 Aug 2022 19:58:02 +0200 Subject: [PATCH] The enabled state of plugins is now loaded from database rather than config file Signed-off-by: Ferdinand Thiessen --- flaschengeist/app.py | 58 +++++++--------- flaschengeist/config.py | 11 --- flaschengeist/controller/messageController.py | 4 +- flaschengeist/controller/pluginController.py | 69 +++++++++++++++++-- flaschengeist/models/__init__.py | 2 +- flaschengeist/models/notification.py | 13 +++- flaschengeist/models/user.py | 2 + flaschengeist/plugins/__init__.py | 14 +++- flaschengeist/plugins/scheduler.py | 9 +-- flaschengeist/plugins/users/__init__.py | 2 +- 10 files changed, 120 insertions(+), 64 deletions(-) diff --git a/flaschengeist/app.py b/flaschengeist/app.py index f992005..29ece60 100644 --- a/flaschengeist/app.py +++ b/flaschengeist/app.py @@ -11,8 +11,7 @@ from werkzeug.exceptions import HTTPException from flaschengeist import logger from flaschengeist.models import Plugin from flaschengeist.utils.hook import Hook -from flaschengeist.plugins import AuthPlugin -from flaschengeist.config import config, configure_app +from flaschengeist.config import configure_app class CustomJSONEncoder(JSONEncoder): @@ -40,39 +39,30 @@ class CustomJSONEncoder(JSONEncoder): def load_plugins(app: Flask): app.config["FG_PLUGINS"] = {} - for entry_point in entry_points(group="flaschengeist.plugins"): - logger.debug(f"Found plugin: {entry_point.name} ({entry_point.dist.version})") + enabled_plugins = Plugin.query.filter(Plugin.enabled == True).all() + all_plugins = entry_points(group="flaschengeist.plugins") - if entry_point.name == config["FLASCHENGEIST"]["auth"] or ( - entry_point.name in config and config[entry_point.name].get("enabled", False) - ): - logger.debug(f"Load plugin {entry_point.name}") - try: - plugin = entry_point.load()(entry_point, config=config.get(entry_point.name, {})) - if hasattr(plugin, "blueprint") and plugin.blueprint is not None: - app.register_blueprint(plugin.blueprint) - except: - logger.error( - f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.", - exc_info=True, - ) - continue - if isinstance(plugin, AuthPlugin): - if entry_point.name != config["FLASCHENGEIST"]["auth"]: - logger.debug(f"Unload not configured AuthPlugin {entry_point.name}") - del plugin - continue - else: - logger.info(f"Using authentication plugin: {entry_point.name}") - app.config["FG_AUTH_BACKEND"] = plugin - else: - logger.info(f"Using plugin: {entry_point.name}") - app.config["FG_PLUGINS"][entry_point.name] = plugin - else: - logger.debug(f"Skip disabled plugin {entry_point.name}") - if "FG_AUTH_BACKEND" not in app.config: - logger.fatal("No authentication plugin configured or authentication plugin not found") - raise RuntimeError("No authentication plugin configured or authentication plugin not found") + for plugin in 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) + except: + logger.error( + f"Plugin {plugin.name} was enabled, but could not be loaded due to an error.", + exc_info=True, + ) + continue + logger.info(f"Loaded plugin: {plugin.name}") + app.config["FG_PLUGINS"][plugin.name] = loaded def create_app(test_config=None, cli=False): diff --git a/flaschengeist/config.py b/flaschengeist/config.py index 3adae22..712d5d1 100644 --- a/flaschengeist/config.py +++ b/flaschengeist/config.py @@ -77,17 +77,6 @@ def configure_app(app, test_config=None): configure_logger() - # Always enable this builtin plugins! - update_dict( - config, - { - "auth": {"enabled": True}, - "roles": {"enabled": True}, - "users": {"enabled": True}, - "scheduler": {"enabled": True}, - }, - ) - if "secret_key" not in config["FLASCHENGEIST"]: logger.critical("No secret key was configured, please configure one for production systems!") raise RuntimeError("No secret key was configured") diff --git a/flaschengeist/controller/messageController.py b/flaschengeist/controller/messageController.py index 573a149..d9ff78c 100644 --- a/flaschengeist/controller/messageController.py +++ b/flaschengeist/controller/messageController.py @@ -1,5 +1,5 @@ -from flaschengeist.utils.hook import Hook -from flaschengeist.models import User, Role +from ..utils.hook import Hook +from ..models import User, Role class Message: diff --git a/flaschengeist/controller/pluginController.py b/flaschengeist/controller/pluginController.py index 8313935..6c16491 100644 --- a/flaschengeist/controller/pluginController.py +++ b/flaschengeist/controller/pluginController.py @@ -29,9 +29,7 @@ def get_setting(plugin_id: str, name: str, **kwargs): `KeyError` if no such setting exists in the database """ try: - setting = ( - PluginSetting.query.filter(PluginSetting.plugin == plugin_id).filter(PluginSetting.name == name).one() - ) + 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: @@ -49,9 +47,7 @@ def set_setting(plugin_id: str, name: str, value): value: Value to be stored """ setting = ( - PluginSetting.query.filter(PluginSetting.plugin == plugin_id) - .filter(PluginSetting.name == name) - .one_or_none() + PluginSetting.query.filter(PluginSetting.plugin == plugin_id).filter(PluginSetting.name == name).one_or_none() ) if setting is not None: if value is None: @@ -81,3 +77,64 @@ def notify(plugin_id: str, user, text: str, data=None): db.session.add(n) db.session.commit() return n.id + + +@Hook("plugins.installed") +def install_plugin(plugin_name: str): + logger.debug(f"Installing plugin {plugin_name}") + entry_point = entry_points(group="flaschengeist.plugins", name=plugin_name) + 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) + db.session.commit() + return entity + + +@Hook("plugin.uninstalled") +def uninstall_plugin(plugin_id: Union[str, int]): + 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] + db.session.delete(plugin) + db.session.commit() + + +@Hook("plugins.enabled") +def enable_plugin(plugin_id: Union[str, int]): + logger.debug(f"Enabling plugin {plugin_id}") + plugin: 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: + plugin = plugin.get(plugin_id) + if plugin is None: + raise NotFound + plugin.enabled = True + db.session.commit() + + return plugin + + +@Hook("plugins.disabled") +def disable_plugin(plugin_id: Union[str, int]): + 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: + plugin = plugin.get(plugin_id) + if plugin is None: + raise NotFound + plugin.enabled = False + db.session.commit() + + return plugin diff --git a/flaschengeist/models/__init__.py b/flaschengeist/models/__init__.py index 99a5e8d..096ac2e 100644 --- a/flaschengeist/models/__init__.py +++ b/flaschengeist/models/__init__.py @@ -2,4 +2,4 @@ from .session import * from .user import * from .plugin import * from .notification import * -from .image import * \ No newline at end of file +from .image import * diff --git a/flaschengeist/models/notification.py b/flaschengeist/models/notification.py index a25efd4..549a5b7 100644 --- a/flaschengeist/models/notification.py +++ b/flaschengeist/models/notification.py @@ -8,10 +8,17 @@ from ..database.types import Serial, UtcDateTime, ModelSerializeMixin class Notification(db.Model, ModelSerializeMixin): __tablename__ = "notification" id: int = db.Column("id", Serial, primary_key=True) - plugin: str = db.Column(db.String(127), nullable=False) text: str = db.Column(db.Text) data: Any = db.Column(db.PickleType(protocol=4)) time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc) - user_id_: int = db.Column("user_id", Serial, db.ForeignKey("user.id"), nullable=False) - user_: User = db.relationship("User") + user_id_: int = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False) + plugin_id_: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"), nullable=False) + user_: "User" = db.relationship("User") + plugin_: "Plugin" = db.relationship( + "Plugin", backref=db.backref("notifications_", cascade="all, delete, delete-orphan") + ) + + @property + def plugin(self): + return self.plugin_.name diff --git a/flaschengeist/models/user.py b/flaschengeist/models/user.py index c468ebf..578758c 100644 --- a/flaschengeist/models/user.py +++ b/flaschengeist/models/user.py @@ -17,11 +17,13 @@ role_permission_association_table = db.Table( db.Column("permission_id", Serial, db.ForeignKey("permission.id")), ) + 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")) class Role(db.Model, ModelSerializeMixin): diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index 76a35e3..f1a68a0 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -115,10 +115,10 @@ class BasePlugin: ``` """ - def __init__(self, entry_point: EntryPoint, config=None): + def __init__(self, entry_point: EntryPoint): """Constructor called by create_app Args: - config: Dict configuration containing the plugin section + entry_point: EntryPoint from which this plugin was loaded """ self.version = entry_point.dist.version self.name = entry_point.name @@ -127,6 +127,8 @@ class BasePlugin: def install(self): """Installation routine + Also called when updating the plugin, compare `version` and `installed_version`. + Is always called with Flask application context, it is called after the plugin permissions are installed. """ @@ -143,6 +145,14 @@ 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 diff --git a/flaschengeist/plugins/scheduler.py b/flaschengeist/plugins/scheduler.py index a5e6eef..43a0a8b 100644 --- a/flaschengeist/plugins/scheduler.py +++ b/flaschengeist/plugins/scheduler.py @@ -2,6 +2,7 @@ from flask import Blueprint from datetime import datetime, timedelta from flaschengeist import logger +from flaschengeist.config import config from flaschengeist.plugins import BasePlugin from flaschengeist.utils.HTTP import no_content @@ -38,8 +39,8 @@ def scheduled(id: str, replace=False, **kwargs): class SchedulerPlugin(BasePlugin): - def __init__(self, entry_point, config=None): - super().__init__(entry_point, config) + def __init__(self, entry_point): + super().__init__(entry_point) self.blueprint = Blueprint(self.name, __name__) def __view_func(): @@ -52,9 +53,9 @@ class SchedulerPlugin(BasePlugin): except: logger.error("Error while executing scheduled tasks!", exc_info=True) - cron = None if config is None else config.get("cron", "passive_web").lower() + cron = config.get("scheduler", {}).get("cron", "passive_web").lower() - if cron is None or cron == "passive_web": + if cron == "passive_web": self.blueprint.teardown_app_request(__passiv_func) elif cron == "active_web": self.blueprint.add_url_rule("/cron", view_func=__view_func) diff --git a/flaschengeist/plugins/users/__init__.py b/flaschengeist/plugins/users/__init__.py index 3cd44df..7511a3f 100644 --- a/flaschengeist/plugins/users/__init__.py +++ b/flaschengeist/plugins/users/__init__.py @@ -10,7 +10,7 @@ from . import permissions from flaschengeist import logger from flaschengeist.config import config from flaschengeist.plugins import BasePlugin -from flaschengeist.models.user import User +from flaschengeist.models import User from flaschengeist.utils.decorators import login_required, extract_session, headers from flaschengeist.controller import userController from flaschengeist.utils.HTTP import created, no_content