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
|
If not provided the version will be set to the
|
||||||
distribution version of the package providing the module"""
|
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
|
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"""
|
"""Override with path to migration files, if custome db tables are used"""
|
||||||
|
|
||||||
|
@ -102,7 +89,29 @@ class PluginMetadata:
|
||||||
|
|
||||||
class Plugin(PluginMetadata):
|
class Plugin(PluginMetadata):
|
||||||
"""Base class for all Plugins
|
"""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):
|
def __init__(self, metadata: PluginMetadata, config=None):
|
||||||
"""Constructor called by create_app
|
"""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.
|
plugin = PluginMetadata(
|
||||||
"""
|
id = "auth",
|
||||||
from flask import Blueprint, request, jsonify
|
name="auth",
|
||||||
from werkzeug.exceptions import Forbidden, BadRequest, Unauthorized
|
module=__name__ + ".plugin.AuthPlugin"
|
||||||
|
)
|
||||||
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()
|
|
|
@ -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
|
from flaschengeist.plugins import PluginMetadata
|
||||||
|
|
||||||
|
|
||||||
def loader():
|
plugin = PluginMetadata(
|
||||||
from .plugin import AuthLDAP
|
id="auth_ldap",
|
||||||
|
|
||||||
return AuthLDAP
|
)
|
||||||
|
|
||||||
|
|
||||||
Plugin = PluginMetadata(id="auth_ldap", name="auth_ldap", plugin=loader)
|
|
||||||
|
|
Loading…
Reference in New Issue