flaschengeist/flaschengeist/plugins/__init__.py

284 lines
8.7 KiB
Python

"""Flaschengeist Plugins
.. include:: docs/plugin_development.md
"""
from typing import Union
from importlib.metadata import entry_points
from werkzeug.exceptions import NotFound
from werkzeug.datastructures import FileStorage
from flaschengeist.models.plugin import BasePlugin
from flaschengeist.models.user import _Avatar, Permission
from flaschengeist.utils.hook import HookBefore, HookAfter
__all__ = [
"plugins_installed",
"plugins_loaded",
"before_delete_user",
"before_role_updated",
"before_update_user",
"after_role_updated",
"Plugin",
"AuthPlugin",
]
# Documentation hacks, see https://github.com/mitmproxy/pdoc/issues/320
plugins_installed = HookAfter("plugins.installed")
plugins_installed.__doc__ = """Hook decorator for when all plugins are installed
Possible use case would be to populate the database with some presets.
"""
plugins_loaded = HookAfter("plugins.loaded")
plugins_loaded.__doc__ = """Hook decorator for when all plugins are loaded
Possible use case would be to check if a specific other plugin is loaded and change own behavior
Passed args:
- *app:* Current flask app instance (args)
"""
before_role_updated = HookBefore("update_role")
before_role_updated.__doc__ = """Hook decorator for when roles are modified
Passed args:
- *role:* `flaschengeist.models.user.Role` to modify
- *new_name:* New name if the name was changed (*None* if delete)
"""
after_role_updated = HookAfter("update_role")
after_role_updated.__doc__ = """Hook decorator for when roles are modified
Passed args:
- *role:* modified `flaschengeist.models.user.Role`
- *new_name:* New name if the name was changed (*None* if deleted)
"""
before_update_user = HookBefore("update_user")
before_update_user.__doc__ = """Hook decorator, when ever an user update is done, this is called before.
Passed args:
- *user:* `flaschengeist.models.user.User` object
"""
before_delete_user = HookBefore("delete_user")
before_delete_user.__doc__ = """Hook decorator,this is called before an user gets deleted.
Passed args:
- *user:* `flaschengeist.models.user.User` object
"""
class Plugin(BasePlugin):
"""Base class for all Plugins
All plugins must derived from this class.
Optional:
- *blueprint*: `flask.Blueprint` providing your routes
- *permissions*: List of your custom permissions
- *models*: Your models, used for API export
"""
blueprint = None
"""Optional `flask.blueprint` if the plugin uses custom routes"""
models = None
"""Optional module containing the SQLAlchemy models used by the plugin"""
@property
def version(self) -> str:
"""Version of the plugin, loaded from Distribution"""
return self.dist.version
@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
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.
"""
pass
def uninstall(self):
"""Uninstall routine
If the plugin has custom database tables, make sure to remove them.
This can be either done by downgrading the plugin *head* to the *base*.
Or use custom migrations for the uninstall and *stamp* some version.
Is always called with Flask application context.
"""
pass
def notify(self, user, text: str, data=None):
"""Create a new notification for an user
Args:
user: `flaschengeist.models.user.User` to notify
text: Visibile notification text
data: Optional data passed to the notificaton
Returns:
ID of the created `flaschengeist.models.notification.Notification`
Hint: use the data for frontend actions.
"""
from ..controller import pluginController
return pluginController.notify(self.name, user, text, data)
def serialize(self):
"""Serialize a plugin into a dict
Returns:
Dict containing version and permissions of the plugin
"""
return {"version": self.version, "permissions": self.permissions}
def install_permissions(self, permissions: list[str]):
"""Helper for installing a list of strings as permissions
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 `Plugin`
"""
def login(self, login_name, password) -> Union[bool, str]:
"""Login routine, MUST BE IMPLEMENTED!
Args:
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, otherwise the UID is returned
"""
raise NotImplemented
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 user_exists(self, userid) -> bool:
"""Check if user exists on this backend
Args:
userid: Userid to search
Returns:
True or False
"""
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.
User might have roles not existing on the external database, so you might have to create those.
Args:
user: User object
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:
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)
"""
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.
Args:
user: User object
password: string
"""
raise NotImplementedError
def delete_user(self, user):
"""If backend is using (writeable) external data, then delete the user from external database.
Args:
user: User object
"""
raise NotImplementedError
def get_avatar(self, user) -> _Avatar:
"""Retrieve avatar for given user (if supported by auth backend)
Default behavior is to use native Image objects,
so by default this function is never called, as the userController checks
native Image objects first.
Args:
user: User to retrieve the avatar for
Raises:
NotFound: If no avatar found or not implemented
"""
raise NotFound
def set_avatar(self, user, file: FileStorage):
"""Set the avatar for given user (if supported by auth backend)
Default behavior is to use native Image objects stored on the Flaschengeist server
Args:
user: User to set the avatar for
file: `werkzeug.datastructures.FileStorage` uploaded by the user
Raises:
MethodNotAllowed: If not supported by Backend
Any valid HTTP exception
"""
# By default save the image to the avatar,
# deleting would happen by unsetting it
from ..controller import imageController
user.avatar_ = imageController.upload_image(file)
def delete_avatar(self, user):
"""Delete the avatar for given user (if supported by auth backend)
Default behavior is to use the imageController and native Image objects.
Args:
user: Uset to delete the avatar for
Raises:
MethodNotAllowed: If not supported by Backend
"""
user.avatar_ = None