Compare commits
2 Commits
74ca9b247d
...
ee38e46c12
Author | SHA1 | Date |
---|---|---|
Ferdinand Thiessen | ee38e46c12 | |
Ferdinand Thiessen | d3530cc15f |
|
@ -1,4 +1,5 @@
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
alembic_migrations = str(Path(__file__).resolve().parent / "migrations")
|
||||
alembic_migrations_path = str(Path(__file__).resolve().parent / "migrations")
|
||||
alembic_script_path = str(Path(__file__).resolve().parent)
|
||||
|
|
|
@ -5,14 +5,12 @@ 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
|
||||
from flaschengeist.models import Plugin
|
||||
from flaschengeist.controller import pluginController
|
||||
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):
|
||||
|
@ -39,40 +37,29 @@ class CustomJSONEncoder(JSONEncoder):
|
|||
@Hook("plugins.loaded")
|
||||
def load_plugins(app: Flask):
|
||||
app.config["FG_PLUGINS"] = {}
|
||||
all_plugins = entry_points(group="flaschengeist.plugins")
|
||||
|
||||
for entry_point in entry_points(group="flaschengeist.plugins"):
|
||||
logger.debug(f"Found plugin: {entry_point.name} ({entry_point.dist.version})")
|
||||
|
||||
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}")
|
||||
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:
|
||||
plugin = entry_point.load()(entry_point, config=config.get(entry_point.name, {}))
|
||||
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 {entry_point.name} was enabled, but could not be loaded due to an error.",
|
||||
f"Plugin {plugin.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")
|
||||
logger.info(f"Loaded plugin: {plugin.name}")
|
||||
app.config["FG_PLUGINS"][plugin.name] = loaded
|
||||
|
||||
|
||||
def create_app(test_config=None, cli=False):
|
||||
|
|
|
@ -3,7 +3,7 @@ from click.decorators import pass_context
|
|||
from flask.cli import with_appcontext
|
||||
from flask_migrate import upgrade
|
||||
|
||||
from flaschengeist.alembic import alembic_migrations
|
||||
from flaschengeist.alembic import alembic_migrations_path
|
||||
from flaschengeist.cli.plugin_cmd import install_plugin_command
|
||||
from flaschengeist.utils.hook import Hook
|
||||
|
||||
|
@ -14,7 +14,7 @@ from flaschengeist.utils.hook import Hook
|
|||
@Hook("plugins.installed")
|
||||
def install(ctx):
|
||||
# Install database
|
||||
upgrade(alembic_migrations, revision="heads")
|
||||
upgrade(alembic_migrations_path, revision="heads")
|
||||
|
||||
# Install plugins
|
||||
install_plugin_command(ctx, [], True)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -8,14 +8,35 @@ import sqlalchemy
|
|||
from typing import Union
|
||||
from flask import current_app
|
||||
from werkzeug.exceptions import NotFound
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
from .. import logger
|
||||
from ..database import db
|
||||
from ..utils import Hook
|
||||
from ..utils.hook import Hook
|
||||
from ..models import Plugin, PluginSetting, Notification
|
||||
|
||||
|
||||
def get_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
|
||||
|
||||
logger.error("Could not connect to database or database not initialized! No plugins enabled!")
|
||||
logger.debug("Can not query enabled plugins", exc_info=True)
|
||||
enabled_plugins = [
|
||||
PluginStub("auth"),
|
||||
PluginStub("roles"),
|
||||
PluginStub("users"),
|
||||
PluginStub("scheduler"),
|
||||
]
|
||||
return enabled_plugins
|
||||
|
||||
|
||||
def get_setting(plugin_id: str, name: str, **kwargs):
|
||||
"""Get plugin setting from database
|
||||
|
||||
|
@ -29,9 +50,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 +68,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 +98,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
|
||||
|
|
|
@ -4,7 +4,9 @@ from flask_sqlalchemy import SQLAlchemy
|
|||
from importlib.metadata import EntryPoint, entry_points, distribution
|
||||
from sqlalchemy import MetaData
|
||||
|
||||
from flaschengeist.alembic import alembic_script_path
|
||||
from flaschengeist import logger
|
||||
from flaschengeist.controller import pluginController
|
||||
|
||||
# https://alembic.sqlalchemy.org/en/latest/naming.html
|
||||
metadata = MetaData(
|
||||
|
@ -31,25 +33,26 @@ def configure_alembic(config: Config):
|
|||
uninstall can break the alembic version management.
|
||||
"""
|
||||
# Set main script location
|
||||
config.set_main_option(
|
||||
"script_location", str(distribution("flaschengeist").locate_file("") / "flaschengeist" / "alembic")
|
||||
)
|
||||
config.set_main_option("script_location", alembic_script_path)
|
||||
|
||||
# Set Flaschengeist's migrations
|
||||
migrations = [config.get_main_option("script_location") + "/migrations"]
|
||||
|
||||
# Gather all migration paths
|
||||
ep: EntryPoint
|
||||
for ep in entry_points(group="flaschengeist.plugins"):
|
||||
all_plugins = entry_points(group="flaschengeist.plugins")
|
||||
for plugin in pluginController.get_enabled_plugins():
|
||||
entry_point = all_plugins.select(name=plugin.name)
|
||||
if not entry_point:
|
||||
continue
|
||||
try:
|
||||
directory = ep.dist.locate_file("")
|
||||
for loc in ep.module.split(".") + ["migrations"]:
|
||||
directory = entry_point.dist.locate_file("")
|
||||
for loc in entry_point.module.split(".") + ["migrations"]:
|
||||
directory /= loc
|
||||
if directory.exists():
|
||||
logger.debug(f"Adding migration version path {directory}")
|
||||
migrations.append(str(directory.resolve()))
|
||||
except:
|
||||
logger.warning(f"Could not load migrations of plugin {ep.name} for database migration.")
|
||||
logger.warning(f"Could not load migrations of plugin {plugin.name} for database migration.")
|
||||
logger.debug("Plugin loading failed", exc_info=True)
|
||||
|
||||
# write back seperator (we changed it if neither seperator nor locations were specified)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue