The enabled state of plugins is now loaded from database rather than config file

Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2022-08-18 19:58:02 +02:00
parent e41be21c47
commit d3530cc15f
10 changed files with 120 additions and 64 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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