Added more examples for splitted metadata

This commit is contained in:
Ferdinand Thiessen 2022-02-13 19:22:36 +01:00
parent e4c552af01
commit ccb1f9b005
5 changed files with 291 additions and 197 deletions

82
doc/plugin.md Normal file
View File

@ -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():
...
```

View File

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

View File

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

View File

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

View File

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