From 74ca9b247dab49a007946c50b9edb6a923c3ee2d 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 | 61 +++++++++++++++++++ flaschengeist/models/notification.py | 13 +++- flaschengeist/models/user.py | 1 + flaschengeist/plugins/__init__.py | 14 ++++- flaschengeist/plugins/users/__init__.py | 2 +- 8 files changed, 111 insertions(+), 53 deletions(-) diff --git a/flaschengeist/app.py b/flaschengeist/app.py index f992005..6402e1a 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.load()(entry_point) + 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..4d42b76 100644 --- a/flaschengeist/controller/pluginController.py +++ b/flaschengeist/controller/pluginController.py @@ -81,3 +81,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/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..1266b9b 100644 --- a/flaschengeist/models/user.py +++ b/flaschengeist/models/user.py @@ -22,6 +22,7 @@ class Permission(db.Model, ModelSerializeMixin): 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/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