Added more examples for splitted metadata
This commit is contained in:
		
							parent
							
								
									e4c552af01
								
							
						
					
					
						commit
						ccb1f9b005
					
				|  | @ -0,0 +1,82 @@ | |||
| # Example on Plugin Development | ||||
| 
 | ||||
| ## File Structure | ||||
| ``` | ||||
| - root | ||||
|   - flaschengeist_example | ||||
|     - __init__.py | ||||
|     - plugin.py | ||||
|     [- migrations] | ||||
|   - setup.cfg | ||||
| ``` | ||||
| 
 | ||||
| In a minimal example, you would only need two python source files and | ||||
| your setup configuration. | ||||
| If you also use custom database tables, you would also need a directory | ||||
| containing your _alembic_-migration files. | ||||
| 
 | ||||
| ## Source code | ||||
| Code is seperated in two locations, the meta data of your plugin goes into the `__init__.py` and all logic goes into the `plugin.py`, of cause you could add more files, but make sure to not depend on some external packages in the `__init__.py`. | ||||
| 
 | ||||
| ### Project data / setuptools | ||||
| For **Flaschengeist** to find your plugin, you have to | ||||
| install it corretly, especially the entry points have to be set. | ||||
| ```ini | ||||
| [metadata] | ||||
| license = MIT | ||||
| version = 1.0.0 | ||||
| name = flaschengeist-example | ||||
| description = Example plugin for Flaschengeist | ||||
| # ... | ||||
| 
 | ||||
| [options] | ||||
| packages = | ||||
|     flaschengeist_example | ||||
| install_requires = | ||||
|     flaschengeist == 2.0.* | ||||
| 
 | ||||
| [options.entry_points] | ||||
|     # Path to your meta data instance | ||||
|     flaschengeist.plugins = | ||||
|         example = flaschengeist_example:plugin | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| ### Metadata | ||||
| The `__init__.py` contains the plugin meta data. | ||||
| 
 | ||||
| ```python | ||||
| # __init__.py | ||||
| from flaschengeist.plugins import PluginMetadata | ||||
| plugin = PluginMetadata( | ||||
|     id="com.example.plugin", # unique, recommend FQN | ||||
|     name="Example Plugin", # Human readable name | ||||
|     version="1.0.0", # Optional | ||||
|     module="flaschengeist_example.plugin.ExamplePlugin" # module and class of your plugin | ||||
| ) | ||||
| ``` | ||||
| 
 | ||||
| `version` is optional, if not provided the version of your distribution (set in setup.cfg) is used. | ||||
| 
 | ||||
| ### Implementation | ||||
| You plugin implementation goes into the `plugin.py`. | ||||
| Here you define your Plugin class, make sure it inheritates from `flaschengeist.plugins.Plugin` or `flaschengeist.plugins.AuthPlugin` respectivly. | ||||
| 
 | ||||
| It will get initalized with the meta data you defined in the `__init__.py` and the configuration object. | ||||
| 
 | ||||
| ```python | ||||
| from flaschengeist.plugins import Plugin | ||||
| from flask import Blueprint | ||||
| 
 | ||||
| bp = Blueprint(...) | ||||
| 
 | ||||
| class ExamplePlugin(Plugin): | ||||
|     blueprint = bp | ||||
|     ... | ||||
| 
 | ||||
| @bp.route("/example") | ||||
| def get_example(): | ||||
|     ... | ||||
| 
 | ||||
| ``` | ||||
|  | @ -68,19 +68,6 @@ class PluginMetadata: | |||
|     If not provided the version will be set to the | ||||
|     distribution version of the package providing the module""" | ||||
| 
 | ||||
|     blueprint: Blueprint = None | ||||
|     """Override with a `flask.blueprint` if the plugin uses custom routes""" | ||||
| 
 | ||||
|     permissions: list[str] = field(default_factory=list) | ||||
|     """Override to add custom permissions used by the plugin | ||||
|      | ||||
|     A good style is to name the permissions with a prefix related to the plugin name, | ||||
|     to prevent clashes with other plugins. E. g. instead of *delete* use *plugin_delete*. | ||||
|     """ | ||||
| 
 | ||||
|     models = None  # You have to override | ||||
|     """Override with models module""" | ||||
| 
 | ||||
|     migrations_path = None  # Override this with the location of your db migrations directory | ||||
|     """Override with path to migration files, if custome db tables are used""" | ||||
| 
 | ||||
|  | @ -102,7 +89,29 @@ class PluginMetadata: | |||
| 
 | ||||
| class Plugin(PluginMetadata): | ||||
|     """Base class for all Plugins | ||||
|     If your class uses custom models add a static property called ``models``""" | ||||
| 
 | ||||
|     All plugins must interitate from this class for adding their logic. | ||||
|     This class gets filled with the meta data of the plugin automatically. | ||||
| 
 | ||||
|     Additionally the `flask.Blueprint` can be added for API routes. | ||||
|     """ | ||||
| 
 | ||||
|     blueprint: Blueprint = None | ||||
|     """Override with a `flask.blueprint` if the plugin uses custom routes""" | ||||
| 
 | ||||
|     models = None | ||||
|     """Module with all custom sqlalchemy models | ||||
|      | ||||
|     Override this with the module containing all your sqlalchemy models module | ||||
|     to enable the `flaschengeist` tool to generate TS API. | ||||
|     """ | ||||
| 
 | ||||
|     permissions: list[str] = [] | ||||
|     """Override to add custom permissions used by the plugin | ||||
|      | ||||
|     A good style is to name the permissions with a prefix related to the plugin name, | ||||
|     to prevent clashes with other plugins. E. g. instead of *delete* use *plugin_delete*. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, metadata: PluginMetadata, config=None): | ||||
|         """Constructor called by create_app | ||||
|  |  | |||
|  | @ -1,177 +1,7 @@ | |||
| """Authentication plugin, provides basic routes | ||||
| from flaschengeist.plugins import PluginMetadata | ||||
| 
 | ||||
| Allow management of authentication, login, logout, etc. | ||||
| """ | ||||
| from flask import Blueprint, request, jsonify | ||||
| from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized | ||||
| 
 | ||||
| from flaschengeist import logger | ||||
| from flaschengeist.plugins import Plugin | ||||
| from flaschengeist.utils.HTTP import no_content, created | ||||
| from flaschengeist.utils.decorators import login_required | ||||
| from flaschengeist.controller import sessionController, userController | ||||
| 
 | ||||
| 
 | ||||
| class AuthRoutePlugin(Plugin): | ||||
|     name = "auth" | ||||
|     blueprint = Blueprint(name, __name__) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth", methods=["POST"]) | ||||
| def login(): | ||||
|     """Login in an user and create a session | ||||
| 
 | ||||
|     Route: ``/auth`` | Method: ``POST`` | ||||
| 
 | ||||
|     POST-data: ``{userid: string, password: string}`` | ||||
| 
 | ||||
|     Returns: | ||||
|         A JSON object with `flaschengeist.models.user.User` and created | ||||
|         `flaschengeist.models.session.Session` or HTTP error | ||||
|     """ | ||||
|     logger.debug("Start log in.") | ||||
|     data = request.get_json() | ||||
|     try: | ||||
|         userid = str(data["userid"]) | ||||
|         password = str(data["password"]) | ||||
|     except (KeyError, ValueError, TypeError): | ||||
|         raise BadRequest("Missing parameter(s)") | ||||
| 
 | ||||
|     logger.debug(f"search user {userid} in database") | ||||
|     user = userController.login_user(userid, password) | ||||
|     if not user: | ||||
|         raise Unauthorized | ||||
|     session = sessionController.create(user, user_agent=request.user_agent) | ||||
|     logger.debug(f"token is {session.token}") | ||||
|     logger.info(f"User {userid} logged in.") | ||||
| 
 | ||||
|     # Lets cleanup the DB | ||||
|     sessionController.clear_expired() | ||||
|     return created(session) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_sessions(current_session, **kwargs): | ||||
|     """Get all valid sessions of current user | ||||
| 
 | ||||
|     Route: ``/auth`` | Method: ``GET`` | ||||
| 
 | ||||
|     Returns: | ||||
|         A JSON array of `flaschengeist.models.session.Session` or HTTP error | ||||
|     """ | ||||
|     sessions = sessionController.get_users_sessions(current_session.user_) | ||||
|     return jsonify(sessions) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["DELETE"]) | ||||
| @login_required() | ||||
| def delete_session(token, current_session, **kwargs): | ||||
|     """Delete a session aka "logout" | ||||
| 
 | ||||
|     Route: ``/auth/<token>`` | Method: ``DELETE`` | ||||
| 
 | ||||
|     Returns: | ||||
|             200 Status (empty) or HTTP error | ||||
|     """ | ||||
|     logger.debug("Try to delete access token {{ {} }}".format(token)) | ||||
|     session = sessionController.get_session(token, current_session.user_) | ||||
|     if not session: | ||||
|         logger.debug("Token not found in database!") | ||||
|         # Return 403 error, so that users can not bruteforce tokens | ||||
|         # Valid tokens from other users and invalid tokens now are looking the same | ||||
|         raise Forbidden | ||||
|     sessionController.delete_session(session) | ||||
|     sessionController.clear_expired() | ||||
|     return "" | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_session(token, current_session, **kwargs): | ||||
|     """Retrieve information about a session | ||||
| 
 | ||||
|     Route: ``/auth/<token>`` | Method: ``GET`` | ||||
| 
 | ||||
|     Attributes: | ||||
|         token: Token identifying session to retrieve | ||||
|         current_session: Session sent with Authorization Header | ||||
| 
 | ||||
|     Returns: | ||||
|         JSON encoded `flaschengeist.models.session.Session` or HTTP error | ||||
|     """ | ||||
|     logger.debug("get token {{ {} }}".format(token)) | ||||
|     session = sessionController.get_session(token, current_session.user_) | ||||
|     if not session: | ||||
|         # Return 403 error, so that users can not bruteforce tokens | ||||
|         # Valid tokens from other users and invalid tokens now are looking the same | ||||
|         raise Forbidden | ||||
|     return jsonify(session) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["PUT"]) | ||||
| @login_required() | ||||
| def set_lifetime(token, current_session, **kwargs): | ||||
|     """Set lifetime of a session | ||||
| 
 | ||||
|     Route: ``/auth/<token>`` | Method: ``PUT`` | ||||
| 
 | ||||
|     POST-data: ``{value: int}`` | ||||
| 
 | ||||
|     Attributes: | ||||
|         token: Token identifying the session | ||||
|         current_session: Session sent with Authorization Header | ||||
| 
 | ||||
|     Returns: | ||||
|         HTTP-204 or HTTP error | ||||
|     """ | ||||
|     session = sessionController.get_session(token, current_session.user_) | ||||
|     if not session: | ||||
|         # Return 403 error, so that users can not bruteforce tokens | ||||
|         # Valid tokens from other users and invalid tokens now are looking the same | ||||
|         raise Forbidden | ||||
|     try: | ||||
|         lifetime = request.get_json()["value"] | ||||
|         logger.debug(f"set lifetime >{lifetime}< to access token >{token}<") | ||||
|         sessionController.set_lifetime(session, lifetime) | ||||
|         return jsonify(sessionController.get_session(token, current_session.user_)) | ||||
|     except (KeyError, TypeError): | ||||
|         raise BadRequest | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>/user", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_assocd_user(token, current_session, **kwargs): | ||||
|     """Retrieve user owning a session | ||||
| 
 | ||||
|     Route: ``/auth/<token>/user`` | Method: ``GET`` | ||||
| 
 | ||||
|     Attributes: | ||||
|         token: Token identifying the session | ||||
|         current_session: Session sent with Authorization Header | ||||
| 
 | ||||
|     Returns: | ||||
|         JSON encoded `flaschengeist.models.user.User` or HTTP error | ||||
|     """ | ||||
|     logger.debug("get token {{ {} }}".format(token)) | ||||
|     session = sessionController.get_session(token, current_session.user_) | ||||
|     if not session: | ||||
|         # Return 403 error, so that users can not bruteforce tokens | ||||
|         # Valid tokens from other users and invalid tokens now are looking the same | ||||
|         raise Forbidden | ||||
|     return jsonify(session.user_) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/reset", methods=["POST"]) | ||||
| def reset_password(): | ||||
|     data = request.get_json() | ||||
|     if "userid" in data: | ||||
|         user = userController.find_user(data["userid"]) | ||||
|         if user: | ||||
|             userController.request_reset(user) | ||||
|     elif "password" in data and "token" in data: | ||||
|         userController.reset_password(data["token"], data["password"]) | ||||
|     else: | ||||
|         raise BadRequest("Missing parameter(s)") | ||||
| 
 | ||||
|     return no_content() | ||||
| plugin = PluginMetadata( | ||||
|     id = "auth", | ||||
|     name="auth", | ||||
|     module=__name__ + ".plugin.AuthPlugin" | ||||
| ) | ||||
|  | @ -0,0 +1,176 @@ | |||
| """Authentication plugin, provides basic routes | ||||
| 
 | ||||
| Allow management of authentication, login, logout, etc. | ||||
| """ | ||||
| from flask import Blueprint, request, jsonify | ||||
| from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized | ||||
| 
 | ||||
| from flaschengeist import logger | ||||
| from flaschengeist.plugins import Plugin | ||||
| from flaschengeist.utils.HTTP import no_content, created | ||||
| from flaschengeist.utils.decorators import login_required | ||||
| from flaschengeist.controller import sessionController, userController | ||||
| 
 | ||||
| 
 | ||||
| class AuthRoutePlugin(Plugin): | ||||
|     blueprint = Blueprint("auth", __name__) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth", methods=["POST"]) | ||||
| def login(): | ||||
|     """Login in an user and create a session | ||||
| 
 | ||||
|     Route: ``/auth`` | Method: ``POST`` | ||||
| 
 | ||||
|     POST-data: ``{userid: string, password: string}`` | ||||
| 
 | ||||
|     Returns: | ||||
|         A JSON object with `flaschengeist.models.user.User` and created | ||||
|         `flaschengeist.models.session.Session` or HTTP error | ||||
|     """ | ||||
|     logger.debug("Start log in.") | ||||
|     data = request.get_json() | ||||
|     try: | ||||
|         userid = str(data["userid"]) | ||||
|         password = str(data["password"]) | ||||
|     except (KeyError, ValueError, TypeError): | ||||
|         raise BadRequest("Missing parameter(s)") | ||||
| 
 | ||||
|     logger.debug(f"search user {userid} in database") | ||||
|     user = userController.login_user(userid, password) | ||||
|     if not user: | ||||
|         raise Unauthorized | ||||
|     session = sessionController.create(user, user_agent=request.user_agent) | ||||
|     logger.debug(f"token is {session.token}") | ||||
|     logger.info(f"User {userid} logged in.") | ||||
| 
 | ||||
|     # Lets cleanup the DB | ||||
|     sessionController.clear_expired() | ||||
|     return created(session) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_sessions(current_session, **kwargs): | ||||
|     """Get all valid sessions of current user | ||||
| 
 | ||||
|     Route: ``/auth`` | Method: ``GET`` | ||||
| 
 | ||||
|     Returns: | ||||
|         A JSON array of `flaschengeist.models.session.Session` or HTTP error | ||||
|     """ | ||||
|     sessions = sessionController.get_users_sessions(current_session.user_) | ||||
|     return jsonify(sessions) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["DELETE"]) | ||||
| @login_required() | ||||
| def delete_session(token, current_session, **kwargs): | ||||
|     """Delete a session aka "logout" | ||||
| 
 | ||||
|     Route: ``/auth/<token>`` | Method: ``DELETE`` | ||||
| 
 | ||||
|     Returns: | ||||
|             200 Status (empty) or HTTP error | ||||
|     """ | ||||
|     logger.debug("Try to delete access token {{ {} }}".format(token)) | ||||
|     session = sessionController.get_session(token, current_session.user_) | ||||
|     if not session: | ||||
|         logger.debug("Token not found in database!") | ||||
|         # Return 403 error, so that users can not bruteforce tokens | ||||
|         # Valid tokens from other users and invalid tokens now are looking the same | ||||
|         raise Forbidden | ||||
|     sessionController.delete_session(session) | ||||
|     sessionController.clear_expired() | ||||
|     return "" | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_session(token, current_session, **kwargs): | ||||
|     """Retrieve information about a session | ||||
| 
 | ||||
|     Route: ``/auth/<token>`` | Method: ``GET`` | ||||
| 
 | ||||
|     Attributes: | ||||
|         token: Token identifying session to retrieve | ||||
|         current_session: Session sent with Authorization Header | ||||
| 
 | ||||
|     Returns: | ||||
|         JSON encoded `flaschengeist.models.session.Session` or HTTP error | ||||
|     """ | ||||
|     logger.debug("get token {{ {} }}".format(token)) | ||||
|     session = sessionController.get_session(token, current_session.user_) | ||||
|     if not session: | ||||
|         # Return 403 error, so that users can not bruteforce tokens | ||||
|         # Valid tokens from other users and invalid tokens now are looking the same | ||||
|         raise Forbidden | ||||
|     return jsonify(session) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["PUT"]) | ||||
| @login_required() | ||||
| def set_lifetime(token, current_session, **kwargs): | ||||
|     """Set lifetime of a session | ||||
| 
 | ||||
|     Route: ``/auth/<token>`` | Method: ``PUT`` | ||||
| 
 | ||||
|     POST-data: ``{value: int}`` | ||||
| 
 | ||||
|     Attributes: | ||||
|         token: Token identifying the session | ||||
|         current_session: Session sent with Authorization Header | ||||
| 
 | ||||
|     Returns: | ||||
|         HTTP-204 or HTTP error | ||||
|     """ | ||||
|     session = sessionController.get_session(token, current_session.user_) | ||||
|     if not session: | ||||
|         # Return 403 error, so that users can not bruteforce tokens | ||||
|         # Valid tokens from other users and invalid tokens now are looking the same | ||||
|         raise Forbidden | ||||
|     try: | ||||
|         lifetime = request.get_json()["value"] | ||||
|         logger.debug(f"set lifetime >{lifetime}< to access token >{token}<") | ||||
|         sessionController.set_lifetime(session, lifetime) | ||||
|         return jsonify(sessionController.get_session(token, current_session.user_)) | ||||
|     except (KeyError, TypeError): | ||||
|         raise BadRequest | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>/user", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_assocd_user(token, current_session, **kwargs): | ||||
|     """Retrieve user owning a session | ||||
| 
 | ||||
|     Route: ``/auth/<token>/user`` | Method: ``GET`` | ||||
| 
 | ||||
|     Attributes: | ||||
|         token: Token identifying the session | ||||
|         current_session: Session sent with Authorization Header | ||||
| 
 | ||||
|     Returns: | ||||
|         JSON encoded `flaschengeist.models.user.User` or HTTP error | ||||
|     """ | ||||
|     logger.debug("get token {{ {} }}".format(token)) | ||||
|     session = sessionController.get_session(token, current_session.user_) | ||||
|     if not session: | ||||
|         # Return 403 error, so that users can not bruteforce tokens | ||||
|         # Valid tokens from other users and invalid tokens now are looking the same | ||||
|         raise Forbidden | ||||
|     return jsonify(session.user_) | ||||
| 
 | ||||
| 
 | ||||
| @AuthRoutePlugin.blueprint.route("/auth/reset", methods=["POST"]) | ||||
| def reset_password(): | ||||
|     data = request.get_json() | ||||
|     if "userid" in data: | ||||
|         user = userController.find_user(data["userid"]) | ||||
|         if user: | ||||
|             userController.request_reset(user) | ||||
|     elif "password" in data and "token" in data: | ||||
|         userController.reset_password(data["token"], data["password"]) | ||||
|     else: | ||||
|         raise BadRequest("Missing parameter(s)") | ||||
| 
 | ||||
|     return no_content() | ||||
|  | @ -1,10 +1,7 @@ | |||
| from flaschengeist.plugins import PluginMetadata | ||||
| 
 | ||||
| 
 | ||||
| def loader(): | ||||
|     from .plugin import AuthLDAP | ||||
| plugin = PluginMetadata( | ||||
|     id="auth_ldap", | ||||
|      | ||||
|     return AuthLDAP | ||||
| 
 | ||||
| 
 | ||||
| Plugin = PluginMetadata(id="auth_ldap", name="auth_ldap", plugin=loader) | ||||
| ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue