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
|
||||
|
||||
return AuthLDAP
|
||||
|
||||
|
||||
Plugin = PluginMetadata(id="auth_ldap", name="auth_ldap", plugin=loader)
|
||||
plugin = PluginMetadata(
|
||||
id="auth_ldap",
|
||||
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue