Compare commits
10 Commits
1b80e39648
...
bd371dfcf2
Author | SHA1 | Date |
---|---|---|
Ferdinand Thiessen | bd371dfcf2 | |
Ferdinand Thiessen | 9f6aa38925 | |
Ferdinand Thiessen | ec7bf39666 | |
Ferdinand Thiessen | 0a3da51b92 | |
Ferdinand Thiessen | ceba819ca7 | |
Ferdinand Thiessen | a3ccd6cea1 | |
Ferdinand Thiessen | 53ed2d9d1a | |
Ferdinand Thiessen | ece6893675 | |
Ferdinand Thiessen | 38ebaf0e79 | |
Ferdinand Thiessen | cfac55efe0 |
|
@ -12,6 +12,7 @@ from . import logger
|
||||||
from .plugins import AuthPlugin
|
from .plugins import AuthPlugin
|
||||||
from flaschengeist.config import config, configure_app
|
from flaschengeist.config import config, configure_app
|
||||||
from flaschengeist.controller import roleController
|
from flaschengeist.controller import roleController
|
||||||
|
from flaschengeist.utils.hook import Hook
|
||||||
|
|
||||||
|
|
||||||
class CustomJSONEncoder(JSONEncoder):
|
class CustomJSONEncoder(JSONEncoder):
|
||||||
|
@ -35,6 +36,7 @@ class CustomJSONEncoder(JSONEncoder):
|
||||||
return JSONEncoder.default(self, o)
|
return JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
|
@Hook("plugins.loaded")
|
||||||
def __load_plugins(app):
|
def __load_plugins(app):
|
||||||
logger.debug("Search for plugins")
|
logger.debug("Search for plugins")
|
||||||
|
|
||||||
|
@ -70,31 +72,30 @@ def __load_plugins(app):
|
||||||
else:
|
else:
|
||||||
logger.info(f"Using plugin: {entry_point.name}")
|
logger.info(f"Using plugin: {entry_point.name}")
|
||||||
app.config["FG_PLUGINS"][entry_point.name] = plugin
|
app.config["FG_PLUGINS"][entry_point.name] = plugin
|
||||||
|
else:
|
||||||
|
logger.debug(f"Skip disabled plugin {entry_point.name}")
|
||||||
if "FG_AUTH_BACKEND" not in app.config:
|
if "FG_AUTH_BACKEND" not in app.config:
|
||||||
logger.error("No authentication plugin configured or authentication plugin not found")
|
logger.error("No authentication plugin configured or authentication plugin not found")
|
||||||
raise RuntimeError("No authentication plugin configured or authentication plugin not found")
|
raise RuntimeError("No authentication plugin configured or authentication plugin not found")
|
||||||
|
|
||||||
|
|
||||||
|
@Hook("plugins.installed")
|
||||||
def install_all():
|
def install_all():
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
|
|
||||||
db.create_all()
|
db.create_all()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
installed = []
|
|
||||||
for name, plugin in current_app.config["FG_PLUGINS"].items():
|
for name, plugin in current_app.config["FG_PLUGINS"].items():
|
||||||
if not plugin:
|
if not plugin:
|
||||||
logger.debug(f"Skip disabled plugin: {name}")
|
logger.debug(f"Skip disabled plugin: {name}")
|
||||||
continue
|
continue
|
||||||
logger.info(f"Install plugin {name}")
|
logger.info(f"Install plugin {name}")
|
||||||
plugin.install()
|
plugin.install()
|
||||||
installed.append(plugin)
|
|
||||||
if plugin.permissions:
|
if plugin.permissions:
|
||||||
roleController.create_permissions(plugin.permissions)
|
roleController.create_permissions(plugin.permissions)
|
||||||
for plugin in installed:
|
|
||||||
plugin.post_install()
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(test_config=None):
|
def create_app(test_config=None, cli=False):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.json_encoder = CustomJSONEncoder
|
app.json_encoder = CustomJSONEncoder
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
@ -102,7 +103,7 @@ def create_app(test_config=None):
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
|
|
||||||
configure_app(app, test_config)
|
configure_app(app, test_config, cli)
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
__load_plugins(app)
|
__load_plugins(app)
|
||||||
|
|
||||||
|
@ -112,14 +113,6 @@ def create_app(test_config=None):
|
||||||
|
|
||||||
return jsonify({"plugins": app.config["FG_PLUGINS"], "version": version})
|
return jsonify({"plugins": app.config["FG_PLUGINS"], "version": version})
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def after_request(response):
|
|
||||||
header = response.headers
|
|
||||||
header["Access-Control-Allow-Origin"] = "*"
|
|
||||||
header["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST,PUT"
|
|
||||||
header["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Authorization"
|
|
||||||
return response
|
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
@app.errorhandler(Exception)
|
||||||
def handle_exception(e):
|
def handle_exception(e):
|
||||||
if isinstance(e, HTTPException):
|
if isinstance(e, HTTPException):
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceGenerator:
|
||||||
|
known = []
|
||||||
|
classes = {}
|
||||||
|
mapper = {
|
||||||
|
"str": "string",
|
||||||
|
"int": "number",
|
||||||
|
"float": "number",
|
||||||
|
"date": "Date",
|
||||||
|
"datetime": "Date",
|
||||||
|
"NoneType": "null",
|
||||||
|
"bool": "boolean",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, namespace, filename, logger=logging.getLogger()):
|
||||||
|
self.basename = ""
|
||||||
|
self.namespace = namespace
|
||||||
|
self.filename = filename
|
||||||
|
self.this_type = None
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def pytype(self, cls):
|
||||||
|
a = self._pytype(cls)
|
||||||
|
return a
|
||||||
|
|
||||||
|
def _pytype(self, cls):
|
||||||
|
import typing
|
||||||
|
|
||||||
|
origin = typing.get_origin(cls)
|
||||||
|
arguments = typing.get_args(cls)
|
||||||
|
|
||||||
|
if origin is typing.ForwardRef: # isinstance(cls, typing.ForwardRef):
|
||||||
|
return "", "this" if cls.__forward_arg__ == self.this_type else cls.__forward_arg__
|
||||||
|
if origin is typing.Union:
|
||||||
|
|
||||||
|
if len(arguments) == 2 and arguments[1] is type(None):
|
||||||
|
return "?", self.pytype(arguments[0])[1]
|
||||||
|
else:
|
||||||
|
return "", "|".join([self.pytype(pt)[1] for pt in arguments])
|
||||||
|
if origin is list:
|
||||||
|
return "", "Array<{}>".format("|".join([self.pytype(a_type)[1] for a_type in arguments]))
|
||||||
|
if cls is typing.Any:
|
||||||
|
return "", "any"
|
||||||
|
|
||||||
|
name = cls.__name__ if hasattr(cls, "__name__") else cls if isinstance(cls, str) else None
|
||||||
|
if name is not None:
|
||||||
|
if name in self.mapper:
|
||||||
|
return "", self.mapper[name]
|
||||||
|
else:
|
||||||
|
return "", name
|
||||||
|
self.logger.warning(f"This python version might not detect all types (try >= 3.9). Could not identify >{cls}<")
|
||||||
|
return "?", "any"
|
||||||
|
|
||||||
|
def walker(self, module):
|
||||||
|
if sys.version_info < (3, 9):
|
||||||
|
raise RuntimeError("Python >= 3.9 is required to export API")
|
||||||
|
import typing
|
||||||
|
|
||||||
|
if (
|
||||||
|
inspect.ismodule(module[1])
|
||||||
|
and module[1].__name__.startswith(self.basename)
|
||||||
|
and module[1].__name__ not in self.known
|
||||||
|
):
|
||||||
|
self.known.append(module[1].__name__)
|
||||||
|
for cls in inspect.getmembers(module[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)):
|
||||||
|
self.walker(cls)
|
||||||
|
elif (
|
||||||
|
inspect.isclass(module[1])
|
||||||
|
and module[1].__module__.startswith(self.basename)
|
||||||
|
and module[0] not in self.classes
|
||||||
|
and not module[0].startswith("_")
|
||||||
|
and hasattr(module[1], "__annotations__")
|
||||||
|
):
|
||||||
|
self.this_type = module[0]
|
||||||
|
|
||||||
|
d = {}
|
||||||
|
for param, ptype in typing.get_type_hints(module[1], globalns=None, localns=None).items():
|
||||||
|
if not param.startswith("_") and not param.endswith("_"):
|
||||||
|
|
||||||
|
d[param] = self.pytype(ptype)
|
||||||
|
|
||||||
|
if len(d) == 1:
|
||||||
|
key, value = d.popitem()
|
||||||
|
self.classes[module[0]] = value[1]
|
||||||
|
else:
|
||||||
|
self.classes[module[0]] = d
|
||||||
|
|
||||||
|
def run(self, models):
|
||||||
|
self.basename = models.__name__
|
||||||
|
self.walker(("models", models))
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
with (open(self.filename, "w") if self.filename else sys.stdout) as file:
|
||||||
|
file.write("declare namespace {} {{\n".format(self.namespace))
|
||||||
|
for cls, params in self.classes.items():
|
||||||
|
if isinstance(params, str):
|
||||||
|
file.write("\ttype {} = {};\n".format(cls, params))
|
||||||
|
else:
|
||||||
|
file.write("\tinterface {} {{\n".format(cls))
|
||||||
|
for name in params:
|
||||||
|
file.write("\t\t{}{}: {};\n".format(name, *params[name]))
|
||||||
|
file.write("\t}\n")
|
||||||
|
file.write("}\n")
|
|
@ -0,0 +1,160 @@
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import click
|
||||||
|
from os import environ
|
||||||
|
from flask import current_app
|
||||||
|
from flask.cli import FlaskGroup, run_command, with_appcontext
|
||||||
|
import pkg_resources
|
||||||
|
from flaschengeist.app import create_app
|
||||||
|
from flaschengeist.config import configure_logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(ctx, param, value):
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
|
||||||
|
import platform
|
||||||
|
from werkzeug import __version__ as werkzeug_version
|
||||||
|
from flask import __version__ as flask_version
|
||||||
|
from flaschengeist import __version__
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
f"Python {platform.python_version()}\n"
|
||||||
|
f"Flask {flask_version}\n"
|
||||||
|
f"Werkzeug {werkzeug_version}\n"
|
||||||
|
f"Flaschengeist {__version__}",
|
||||||
|
color=ctx.color,
|
||||||
|
)
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
|
||||||
|
@with_appcontext
|
||||||
|
def verbosity(ctx, param, value):
|
||||||
|
"""Toggle verbosity between WARNING <-> DEBUG"""
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
configure_logger(cli=40 - max(0, min(value * 10, 30)))
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(
|
||||||
|
cls=FlaskGroup,
|
||||||
|
add_version_option=False,
|
||||||
|
add_default_commands=False,
|
||||||
|
create_app=lambda: create_app(cli=True),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--version",
|
||||||
|
help="Show the flask version",
|
||||||
|
expose_value=False,
|
||||||
|
callback=get_version,
|
||||||
|
is_flag=True,
|
||||||
|
is_eager=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--verbose",
|
||||||
|
"-v",
|
||||||
|
help="Increase logging level",
|
||||||
|
callback=verbosity,
|
||||||
|
count=True,
|
||||||
|
expose_value=False,
|
||||||
|
)
|
||||||
|
def cli():
|
||||||
|
"""Management script for the Flaschengeist application."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@with_appcontext
|
||||||
|
def install():
|
||||||
|
"""Install and initialize enabled plugins.
|
||||||
|
|
||||||
|
Most plugins need to install custom tables into the database
|
||||||
|
running this command will lookup all enabled plugins and run
|
||||||
|
their database initalization routines.
|
||||||
|
"""
|
||||||
|
from flaschengeist.app import install_all
|
||||||
|
|
||||||
|
install_all()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--output", "-o", help="Output file, default is stdout", type=click.Path())
|
||||||
|
@click.option("--namespace", "-n", help="TS namespace for the interfaces", type=str, show_default=True)
|
||||||
|
@click.option("--plugin", "-p", help="Also export types for a plugin (even if disabled)", multiple=True, type=str)
|
||||||
|
@click.option("--no-core", help="Skip models / types from flaschengeist core", is_flag=True)
|
||||||
|
def export(namespace, output, no_core, plugin):
|
||||||
|
from flaschengeist import models
|
||||||
|
from flaschengeist import logger
|
||||||
|
from .InterfaceGenerator import InterfaceGenerator
|
||||||
|
|
||||||
|
gen = InterfaceGenerator(namespace, output, logger)
|
||||||
|
if not no_core:
|
||||||
|
gen.run(models)
|
||||||
|
if plugin:
|
||||||
|
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugins"):
|
||||||
|
if len(plugin) == 0 or entry_point.name in plugin:
|
||||||
|
plg = entry_point.load()
|
||||||
|
if hasattr(plg, "models") and plg.models is not None:
|
||||||
|
gen.run(plg.models)
|
||||||
|
gen.write()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("--host", help="set hostname to listen on", default="127.0.0.1", show_default=True)
|
||||||
|
@click.option("--port", help="set port to listen on", type=int, default=5000, show_default=True)
|
||||||
|
@click.option("--debug", help="run in debug mode", is_flag=True)
|
||||||
|
@with_appcontext
|
||||||
|
@click.pass_context
|
||||||
|
def run(ctx, host, port, debug):
|
||||||
|
"""Run Flaschengeist using a development server."""
|
||||||
|
|
||||||
|
class PrefixMiddleware(object):
|
||||||
|
def __init__(self, app, prefix=""):
|
||||||
|
self.app = app
|
||||||
|
self.prefix = prefix
|
||||||
|
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
|
||||||
|
if environ["PATH_INFO"].startswith(self.prefix):
|
||||||
|
environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :]
|
||||||
|
environ["SCRIPT_NAME"] = self.prefix
|
||||||
|
return self.app(environ, start_response)
|
||||||
|
else:
|
||||||
|
start_response("404", [("Content-Type", "text/plain")])
|
||||||
|
return ["This url does not belong to the app.".encode()]
|
||||||
|
|
||||||
|
from flaschengeist.config import config
|
||||||
|
|
||||||
|
# re configure logger, as we are no logger in CLI mode
|
||||||
|
configure_logger()
|
||||||
|
current_app.wsgi_app = PrefixMiddleware(current_app.wsgi_app, prefix=config["FLASCHENGEIST"].get("root", ""))
|
||||||
|
if debug:
|
||||||
|
environ["FLASK_DEBUG"] = "1"
|
||||||
|
environ["FLASK_ENV"] = "development"
|
||||||
|
|
||||||
|
ctx.invoke(run_command, host=host, port=port, debugger=debug)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--output",
|
||||||
|
"-o",
|
||||||
|
help="Documentation output path",
|
||||||
|
default="./docs",
|
||||||
|
type=click.Path(file_okay=False, path_type=pathlib.Path),
|
||||||
|
)
|
||||||
|
def docs(output: pathlib.Path):
|
||||||
|
"""Generate and export API documentation using pdoc3"""
|
||||||
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
|
command = [
|
||||||
|
"python",
|
||||||
|
"-m",
|
||||||
|
"pdoc",
|
||||||
|
"--skip-errors",
|
||||||
|
"--html",
|
||||||
|
"--output-dir",
|
||||||
|
str(output),
|
||||||
|
"flaschengeist",
|
||||||
|
]
|
||||||
|
click.echo(f"Running command: {command}")
|
||||||
|
subprocess.check_call(command)
|
|
@ -33,7 +33,7 @@ def read_configuration(test_config):
|
||||||
for loc in paths:
|
for loc in paths:
|
||||||
try:
|
try:
|
||||||
with (loc / "flaschengeist.toml").open() as source:
|
with (loc / "flaschengeist.toml").open() as source:
|
||||||
print("Reading config file from >{}<".format(loc))
|
logger.warning(f"Reading config file from >{loc}<") # default root logger, goes to stderr
|
||||||
update_dict(config, toml.load(source))
|
update_dict(config, toml.load(source))
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
@ -41,7 +41,7 @@ def read_configuration(test_config):
|
||||||
update_dict(config, test_config)
|
update_dict(config, test_config)
|
||||||
|
|
||||||
|
|
||||||
def configure_logger():
|
def configure_logger(cli=False):
|
||||||
global config
|
global config
|
||||||
# Read default config
|
# Read default config
|
||||||
logger_config = toml.load(_module_path / "logging.toml")
|
logger_config = toml.load(_module_path / "logging.toml")
|
||||||
|
@ -50,25 +50,27 @@ def configure_logger():
|
||||||
# Override with user config
|
# Override with user config
|
||||||
update_dict(logger_config, config.get("LOGGING"))
|
update_dict(logger_config, config.get("LOGGING"))
|
||||||
# Check for shortcuts
|
# Check for shortcuts
|
||||||
if "level" in config["LOGGING"]:
|
if "level" in config["LOGGING"] or isinstance(cli, int):
|
||||||
logger_config["loggers"]["flaschengeist"] = {"level": config["LOGGING"]["level"]}
|
level = cli if isinstance(cli, int) else config["LOGGING"]["level"]
|
||||||
logger_config["handlers"]["console"]["level"] = config["LOGGING"]["level"]
|
logger_config["loggers"]["flaschengeist"] = {"level": level}
|
||||||
logger_config["handlers"]["file"]["level"] = config["LOGGING"]["level"]
|
logger_config["handlers"]["console"]["level"] = level
|
||||||
if not config["LOGGING"].get("console", True):
|
logger_config["handlers"]["file"]["level"] = level
|
||||||
|
if cli is True or not config["LOGGING"].get("console", True):
|
||||||
logger_config["handlers"]["console"]["level"] = "CRITICAL"
|
logger_config["handlers"]["console"]["level"] = "CRITICAL"
|
||||||
if "file" in config["LOGGING"]:
|
if not cli and isinstance(config["LOGGING"].get("file", False), str):
|
||||||
logger_config["root"]["handlers"].append("file")
|
logger_config["root"]["handlers"].append("file")
|
||||||
logger_config["handlers"]["file"]["filename"] = config["LOGGING"]["file"]
|
logger_config["handlers"]["file"]["filename"] = config["LOGGING"]["file"]
|
||||||
path = Path(config["LOGGING"]["file"])
|
Path(config["LOGGING"]["file"]).parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
else:
|
||||||
|
del logger_config["handlers"]["file"]
|
||||||
logging.config.dictConfig(logger_config)
|
logging.config.dictConfig(logger_config)
|
||||||
|
|
||||||
|
|
||||||
def configure_app(app, test_config=None):
|
def configure_app(app, test_config=None, cli=False):
|
||||||
global config
|
global config
|
||||||
read_configuration(test_config)
|
read_configuration(test_config)
|
||||||
|
|
||||||
configure_logger()
|
configure_logger(cli)
|
||||||
|
|
||||||
# Always enable this builtin plugins!
|
# Always enable this builtin plugins!
|
||||||
update_dict(
|
update_dict(
|
||||||
|
|
|
@ -17,7 +17,7 @@ def get(role_name) -> Role:
|
||||||
else:
|
else:
|
||||||
role = Role.query.filter(Role.name == role_name).one_or_none()
|
role = Role.query.filter(Role.name == role_name).one_or_none()
|
||||||
if not role:
|
if not role:
|
||||||
raise NotFound
|
raise NotFound("no such role")
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,11 +56,14 @@ def create_permissions(permissions):
|
||||||
|
|
||||||
def create_role(name: str, permissions=[]):
|
def create_role(name: str, permissions=[]):
|
||||||
logger.debug(f"Create new role with name: {name}")
|
logger.debug(f"Create new role with name: {name}")
|
||||||
role = Role(name=name)
|
try:
|
||||||
db.session.add(role)
|
role = Role(name=name)
|
||||||
set_permissions(role, permissions)
|
db.session.add(role)
|
||||||
db.session.commit()
|
set_permissions(role, permissions)
|
||||||
logger.debug(f"Created role: {role.serialize()}")
|
db.session.commit()
|
||||||
|
logger.debug(f"Created role: {role.serialize()}")
|
||||||
|
except IntegrityError:
|
||||||
|
raise BadRequest("role already exists")
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import secrets
|
import secrets
|
||||||
|
import re
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
@ -90,15 +91,13 @@ def update_user(user):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def set_roles(user: User, roles: list[str], create=False):
|
def set_roles(user: User, roles: list[str]):
|
||||||
user.roles_.clear()
|
if not isinstance(roles, list) and any([not isinstance(r, str) for r in roles]):
|
||||||
for role_name in roles:
|
raise BadRequest("Invalid role name")
|
||||||
role = Role.query.filter(Role.name == role_name).one_or_none()
|
fetched = Role.query.filter(Role.name.in_(roles)).all()
|
||||||
if not role:
|
if len(fetched) < len(roles):
|
||||||
if not create:
|
raise BadRequest("Invalid role name, role not found")
|
||||||
raise BadRequest("Role not found >{}<".format(role_name))
|
user.roles_ = fetched
|
||||||
role = Role(name=role_name)
|
|
||||||
user.roles_.append(role)
|
|
||||||
|
|
||||||
|
|
||||||
def modify_user(user, password, new_password=None):
|
def modify_user(user, password, new_password=None):
|
||||||
|
@ -216,29 +215,40 @@ def delete_user(user: User):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def register(data):
|
def register(data, passwd=None):
|
||||||
|
"""Register a new user
|
||||||
|
Args:
|
||||||
|
data: dictionary containing valid user properties
|
||||||
|
passwd: optional a password, default: 16byte random
|
||||||
|
"""
|
||||||
allowed_keys = User().serialize().keys()
|
allowed_keys = User().serialize().keys()
|
||||||
values = {key: value for key, value in data.items() if key in allowed_keys}
|
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||||
roles = values.pop("roles", [])
|
roles = values.pop("roles", [])
|
||||||
if "birthday" in data:
|
if "birthday" in data:
|
||||||
values["birthday"] = from_iso_format(data["birthday"]).date()
|
values["birthday"] = from_iso_format(data["birthday"]).date()
|
||||||
|
if "mail" in data and not re.match(r"[^@]+@[^@]+\.[^@]+", data["mail"]):
|
||||||
|
raise BadRequest("Invalid mail given")
|
||||||
user = User(**values)
|
user = User(**values)
|
||||||
set_roles(user, roles)
|
set_roles(user, roles)
|
||||||
|
|
||||||
password = secrets.token_urlsafe(16)
|
password = passwd if passwd else secrets.token_urlsafe(16)
|
||||||
current_app.config["FG_AUTH_BACKEND"].create_user(user, password)
|
current_app.config["FG_AUTH_BACKEND"].create_user(user, password)
|
||||||
db.session.add(user)
|
try:
|
||||||
db.session.commit()
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
except exc.IntegrityError:
|
||||||
|
raise BadRequest("userid already in use")
|
||||||
|
|
||||||
reset = _generate_password_reset(user)
|
if user.mail:
|
||||||
|
reset = _generate_password_reset(user)
|
||||||
|
|
||||||
subject = str(config["MESSAGES"]["welcome_subject"]).format(name=user.display_name, username=user.userid)
|
subject = str(config["MESSAGES"]["welcome_subject"]).format(name=user.display_name, username=user.userid)
|
||||||
text = str(config["MESSAGES"]["welcome_text"]).format(
|
text = str(config["MESSAGES"]["welcome_text"]).format(
|
||||||
name=user.display_name,
|
name=user.display_name,
|
||||||
username=user.userid,
|
username=user.userid,
|
||||||
password_link=f'https://{config["FLASCHENGEIST"]["domain"]}/reset?token={reset.token}',
|
password_link=f'https://{config["FLASCHENGEIST"]["domain"]}/reset?token={reset.token}',
|
||||||
)
|
)
|
||||||
messageController.send_message(messageController.Message(user, text, subject))
|
messageController.send_message(messageController.Message(user, text, subject))
|
||||||
|
|
||||||
find_user(user.userid)
|
find_user(user.userid)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,31 @@
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from sqlalchemy import MetaData
|
||||||
|
|
||||||
db = SQLAlchemy()
|
# https://alembic.sqlalchemy.org/en/latest/naming.html
|
||||||
|
metadata = MetaData(
|
||||||
|
naming_convention={
|
||||||
|
"pk": "pk_%(table_name)s",
|
||||||
|
"ix": "ix_%(table_name)s_%(column_0_name)s",
|
||||||
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||||
|
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
||||||
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
db = SQLAlchemy(metadata=metadata)
|
||||||
|
|
||||||
|
|
||||||
def case_sensitive(s):
|
def case_sensitive(s):
|
||||||
|
"""
|
||||||
|
Compare string as case sensitive on the database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
s: string to compare
|
||||||
|
|
||||||
|
Example:
|
||||||
|
User.query.filter(User.name == case_sensitive(some_string))
|
||||||
|
"""
|
||||||
if db.session.bind.dialect.name == "mysql":
|
if db.session.bind.dialect.name == "mysql":
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,11 @@ secret_key = "V3ryS3cr3t"
|
||||||
#
|
#
|
||||||
# Logging level, possible: DEBUG INFO WARNING ERROR
|
# Logging level, possible: DEBUG INFO WARNING ERROR
|
||||||
level = "DEBUG"
|
level = "DEBUG"
|
||||||
# Uncomment to enable logging to a file
|
# Logging to a file is simple, just add the path
|
||||||
# file = "/tmp/flaschengeist-debug.log"
|
# file = "/tmp/flaschengeist-debug.log"
|
||||||
|
file = false
|
||||||
# Uncomment to disable console logging
|
# Uncomment to disable console logging
|
||||||
# console = False
|
# console = false
|
||||||
|
|
||||||
[DATABASE]
|
[DATABASE]
|
||||||
# engine = "mysql" (default)
|
# engine = "mysql" (default)
|
||||||
|
|
|
@ -15,7 +15,7 @@ disable_existing_loggers = false
|
||||||
class = "logging.StreamHandler"
|
class = "logging.StreamHandler"
|
||||||
level = "DEBUG"
|
level = "DEBUG"
|
||||||
formatter = "simple"
|
formatter = "simple"
|
||||||
stream = "ext://sys.stdout"
|
stream = "ext://sys.stderr"
|
||||||
[handlers.file]
|
[handlers.file]
|
||||||
class = "logging.handlers.WatchedFileHandler"
|
class = "logging.handlers.WatchedFileHandler"
|
||||||
level = "WARNING"
|
level = "WARNING"
|
||||||
|
|
|
@ -53,7 +53,7 @@ class User(db.Model, ModelSerializeMixin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "user"
|
__tablename__ = "user"
|
||||||
userid: str = db.Column(db.String(30), nullable=False)
|
userid: str = db.Column(db.String(30), unique=True, nullable=False)
|
||||||
display_name: str = db.Column(db.String(30))
|
display_name: str = db.Column(db.String(30))
|
||||||
firstname: str = db.Column(db.String(50), nullable=False)
|
firstname: str = db.Column(db.String(50), nullable=False)
|
||||||
lastname: str = db.Column(db.String(50), nullable=False)
|
lastname: str = db.Column(db.String(50), nullable=False)
|
||||||
|
|
|
@ -10,6 +10,21 @@ from flaschengeist.models.user import _Avatar, User
|
||||||
from flaschengeist.models.setting import _PluginSetting
|
from flaschengeist.models.setting import _PluginSetting
|
||||||
from flaschengeist.utils.hook import HookBefore, HookAfter
|
from flaschengeist.utils.hook import HookBefore, HookAfter
|
||||||
|
|
||||||
|
plugins_installed = HookAfter("plugins.installed")
|
||||||
|
"""Hook decorator for when all plugins are installed
|
||||||
|
Possible use case would be to populate the database with some presets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hook_result: void (kwargs)
|
||||||
|
"""
|
||||||
|
plugins_loaded = HookAfter("plugins.loaded")
|
||||||
|
"""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
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: Current flask app instance (args)
|
||||||
|
hook_result: void (kwargs)
|
||||||
|
"""
|
||||||
before_role_updated = HookBefore("update_role")
|
before_role_updated = HookBefore("update_role")
|
||||||
"""Hook decorator for when roles are modified
|
"""Hook decorator for when roles are modified
|
||||||
Args:
|
Args:
|
||||||
|
@ -57,12 +72,6 @@ class Plugin:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def post_install(self):
|
|
||||||
"""Fill database or do other stuff
|
|
||||||
Called after all plugins are installed
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_setting(self, name: str, **kwargs):
|
def get_setting(self, name: str, **kwargs):
|
||||||
"""Get plugin setting from database
|
"""Get plugin setting from database
|
||||||
Args:
|
Args:
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import click
|
||||||
|
from flask import current_app
|
||||||
|
from flask.cli import with_appcontext
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(no_args_is_help=True)
|
||||||
|
@click.option("--sync", is_flag=True, default=False, help="Synchronize users from LDAP -> database")
|
||||||
|
@with_appcontext
|
||||||
|
@click.pass_context
|
||||||
|
def ldap(ctx, sync):
|
||||||
|
"""Tools for the LDAP authentification"""
|
||||||
|
if sync:
|
||||||
|
from flaschengeist.controller import userController
|
||||||
|
from flaschengeist.plugins.auth_ldap import AuthLDAP
|
||||||
|
from ldap3 import SUBTREE
|
||||||
|
|
||||||
|
auth_ldap: AuthLDAP = current_app.config.get("FG_AUTH_BACKEND")
|
||||||
|
if auth_ldap is None or not isinstance(auth_ldap, AuthLDAP):
|
||||||
|
ctx.fail("auth_ldap plugin not found or not enabled!")
|
||||||
|
conn = auth_ldap.ldap.connection
|
||||||
|
if not conn:
|
||||||
|
conn = auth_ldap.ldap.connect(auth_ldap.root_dn, auth_ldap.root_secret)
|
||||||
|
conn.search(auth_ldap.search_dn, "(uid=*)", SUBTREE, attributes=["uid", "givenName", "sn", "mail"])
|
||||||
|
ldap_users_response = conn.response
|
||||||
|
for ldap_user in ldap_users_response:
|
||||||
|
uid = ldap_user["attributes"]["uid"][0]
|
||||||
|
userController.find_user(uid)
|
|
@ -7,13 +7,16 @@ import os
|
||||||
import hashlib
|
import hashlib
|
||||||
import binascii
|
import binascii
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
from flaschengeist.plugins import AuthPlugin
|
from flaschengeist.plugins import AuthPlugin, plugins_installed
|
||||||
from flaschengeist.models.user import User, Role, Permission
|
from flaschengeist.models.user import User, Role, Permission
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
|
|
||||||
|
|
||||||
class AuthPlain(AuthPlugin):
|
class AuthPlain(AuthPlugin):
|
||||||
|
def install(self):
|
||||||
|
plugins_installed(self.post_install)
|
||||||
|
|
||||||
def post_install(self):
|
def post_install(self):
|
||||||
if User.query.filter(User.deleted == False).count() == 0:
|
if User.query.filter(User.deleted == False).count() == 0:
|
||||||
logger.info("Installing admin user")
|
logger.info("Installing admin user")
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import click
|
||||||
|
from flask.cli import with_appcontext
|
||||||
|
from werkzeug.exceptions import BadRequest, Conflict, NotFound
|
||||||
|
from flaschengeist.controller import roleController, userController
|
||||||
|
|
||||||
|
|
||||||
|
USER_KEY = f"{__name__}.user"
|
||||||
|
|
||||||
|
|
||||||
|
def user(ctx, param, value):
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
|
||||||
|
click.echo("Adding new user")
|
||||||
|
ctx.meta[USER_KEY] = {}
|
||||||
|
try:
|
||||||
|
ctx.meta[USER_KEY]["userid"] = click.prompt("userid", type=str)
|
||||||
|
ctx.meta[USER_KEY]["firstname"] = click.prompt("firstname", type=str)
|
||||||
|
ctx.meta[USER_KEY]["lastname"] = click.prompt("lastname", type=str)
|
||||||
|
ctx.meta[USER_KEY]["display_name"] = click.prompt("displayed name", type=str, default="")
|
||||||
|
ctx.meta[USER_KEY]["mail"] = click.prompt("mail", type=str, default="")
|
||||||
|
ctx.meta[USER_KEY]["password"] = click.prompt("password", type=str, confirmation_prompt=True, hide_input=True)
|
||||||
|
ctx.meta[USER_KEY] = {k: v for k, v in ctx.meta[USER_KEY].items() if v != ""}
|
||||||
|
|
||||||
|
except click.Abort:
|
||||||
|
click.echo("\n!!! User was not added, aborted.")
|
||||||
|
del ctx.meta[USER_KEY]
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option("--add-role", help="Add new role", type=str)
|
||||||
|
@click.option("--set-admin", help="Make a role an admin role, adding all permissions", type=str)
|
||||||
|
@click.option("--add-user", help="Add new user interactivly", callback=user, is_flag=True, expose_value=False)
|
||||||
|
@with_appcontext
|
||||||
|
def users(add_role, set_admin):
|
||||||
|
from flaschengeist.database import db
|
||||||
|
|
||||||
|
ctx = click.get_current_context()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if add_role:
|
||||||
|
roleController.create_role(add_role)
|
||||||
|
if set_admin:
|
||||||
|
role = roleController.get(set_admin)
|
||||||
|
role.permissions = roleController.get_permissions()
|
||||||
|
db.session.commit()
|
||||||
|
if USER_KEY in ctx.meta:
|
||||||
|
userController.register(ctx.meta[USER_KEY], ctx.meta[USER_KEY]["password"])
|
||||||
|
except (BadRequest, NotFound) as e:
|
||||||
|
ctx.fail(e.description)
|
|
@ -1,43 +1,69 @@
|
||||||
_hook_dict = ({}, {})
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
class Hook(object):
|
_hooks_before = {}
|
||||||
"""Decorator for Hooks
|
_hooks_after = {}
|
||||||
Use to decorate system hooks where plugins should be able to hook in
|
|
||||||
|
|
||||||
|
def Hook(function=None, id=None):
|
||||||
|
"""Hook decorator
|
||||||
|
Use to decorate functions as hooks, so plugins can hook up their custom functions.
|
||||||
|
"""
|
||||||
|
# `id` passed as `arg` not `kwarg`
|
||||||
|
if isinstance(function, str):
|
||||||
|
return Hook(id=function)
|
||||||
|
|
||||||
|
def decorator(function):
|
||||||
|
@wraps(function)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
_id = id if id is not None else function.__qualname__
|
||||||
|
# Hooks before
|
||||||
|
for f in _hooks_before.get(_id, []):
|
||||||
|
f(*args, **kwargs)
|
||||||
|
# Main function
|
||||||
|
result = function(*args, **kwargs)
|
||||||
|
# Hooks after
|
||||||
|
for f in _hooks_after.get(_id, []):
|
||||||
|
f(*args, hook_result=result, **kwargs)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
# Called @Hook or we are in the second step
|
||||||
|
if callable(function):
|
||||||
|
return decorator(function)
|
||||||
|
else:
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def HookBefore(id: str):
|
||||||
|
"""Decorator for functions to be called before a Hook-Function is called
|
||||||
|
The hooked up function must accept the same arguments as the function hooked onto,
|
||||||
|
as the functions are called with the same arguments.
|
||||||
|
Hint: This enables you to modify the arguments!
|
||||||
|
"""
|
||||||
|
if not id or not isinstance(id, str):
|
||||||
|
raise TypeError("HookBefore requires the ID of the function to hook up")
|
||||||
|
|
||||||
|
def wrapped(function):
|
||||||
|
_hooks_before.setdefault(id, []).append(function)
|
||||||
|
return function
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def HookAfter(id: str):
|
||||||
|
"""Decorator for functions to be called after a Hook-Function is called
|
||||||
|
As with the HookBefore, the hooked up function must accept the same
|
||||||
|
arguments as the function hooked onto, but also receives a
|
||||||
|
`hook_result` kwarg containing the result of the function.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, function):
|
if not id or not isinstance(id, str):
|
||||||
self.function = function
|
raise TypeError("HookAfter requires the ID of the function to hook up")
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def wrapped(function):
|
||||||
# Hooks before
|
_hooks_after.setdefault(id, []).append(function)
|
||||||
for function in _hook_dict[0].get(self.function.__name__, []):
|
|
||||||
function(*args, **kwargs)
|
|
||||||
# Main function
|
|
||||||
ret = self.function(*args, **kwargs)
|
|
||||||
# Hooks after
|
|
||||||
for function in _hook_dict[1].get(self.function.__name__, []):
|
|
||||||
function(*args, **kwargs)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class HookBefore(object):
|
|
||||||
"""Decorator for functions to be called before a Hook-Function is called"""
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def __call__(self, function):
|
|
||||||
_hook_dict[0].setdefault(self.name, []).append(function)
|
|
||||||
return function
|
return function
|
||||||
|
|
||||||
|
return wrapped
|
||||||
class HookAfter(object):
|
|
||||||
"""Decorator for functions to be called after a Hook-Function is called"""
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def __call__(self, function):
|
|
||||||
_hook_dict[1].setdefault(self.name, []).append(function)
|
|
||||||
return function
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
|
@ -1,226 +0,0 @@
|
||||||
#!/usr/bin/python3
|
|
||||||
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
|
||||||
import inspect
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import pkg_resources
|
|
||||||
from os import environ
|
|
||||||
|
|
||||||
from flaschengeist.app import create_app, install_all
|
|
||||||
from flaschengeist.config import config
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixMiddleware(object):
|
|
||||||
def __init__(self, app, prefix=""):
|
|
||||||
self.app = app
|
|
||||||
self.prefix = prefix
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
|
||||||
|
|
||||||
if environ["PATH_INFO"].startswith(self.prefix):
|
|
||||||
environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :]
|
|
||||||
environ["SCRIPT_NAME"] = self.prefix
|
|
||||||
return self.app(environ, start_response)
|
|
||||||
else:
|
|
||||||
start_response("404", [("Content-Type", "text/plain")])
|
|
||||||
return ["This url does not belong to the app.".encode()]
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceGenerator:
|
|
||||||
known = []
|
|
||||||
classes = {}
|
|
||||||
mapper = {
|
|
||||||
"str": "string",
|
|
||||||
"int": "number",
|
|
||||||
"float": "number",
|
|
||||||
"date": "Date",
|
|
||||||
"datetime": "Date",
|
|
||||||
"NoneType": "null",
|
|
||||||
"bool": "boolean",
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, namespace, filename):
|
|
||||||
self.basename = ""
|
|
||||||
self.namespace = namespace
|
|
||||||
self.filename = filename
|
|
||||||
self.this_type = None
|
|
||||||
|
|
||||||
def pytype(self, cls):
|
|
||||||
a = self._pytype(cls)
|
|
||||||
print(f"{cls} -> {a}")
|
|
||||||
return a
|
|
||||||
|
|
||||||
def _pytype(self, cls):
|
|
||||||
import typing
|
|
||||||
|
|
||||||
origin = typing.get_origin(cls)
|
|
||||||
arguments = typing.get_args(cls)
|
|
||||||
|
|
||||||
if origin is typing.ForwardRef: # isinstance(cls, typing.ForwardRef):
|
|
||||||
return "", "this" if cls.__forward_arg__ == self.this_type else cls.__forward_arg__
|
|
||||||
if origin is typing.Union:
|
|
||||||
print(f"A1: {arguments[1]}")
|
|
||||||
if len(arguments) == 2 and arguments[1] is type(None):
|
|
||||||
return "?", self.pytype(arguments[0])[1]
|
|
||||||
else:
|
|
||||||
return "", "|".join([self.pytype(pt)[1] for pt in arguments])
|
|
||||||
if origin is list:
|
|
||||||
return "", "Array<{}>".format("|".join([self.pytype(a_type)[1] for a_type in arguments]))
|
|
||||||
|
|
||||||
name = cls.__name__ if hasattr(cls, "__name__") else cls if isinstance(cls, str) else None
|
|
||||||
if name is not None:
|
|
||||||
if name in self.mapper:
|
|
||||||
return "", self.mapper[name]
|
|
||||||
else:
|
|
||||||
return "", name
|
|
||||||
print(
|
|
||||||
"WARNING: This python version might not detect all types (try >= 3.9). Could not identify >{}<".format(cls)
|
|
||||||
)
|
|
||||||
return "?", "any"
|
|
||||||
|
|
||||||
def walker(self, module):
|
|
||||||
if sys.version_info < (3, 9):
|
|
||||||
raise RuntimeError("Python >= 3.9 is required to export API")
|
|
||||||
import typing
|
|
||||||
|
|
||||||
if (
|
|
||||||
inspect.ismodule(module[1])
|
|
||||||
and module[1].__name__.startswith(self.basename)
|
|
||||||
and module[1].__name__ not in self.known
|
|
||||||
):
|
|
||||||
self.known.append(module[1].__name__)
|
|
||||||
for cls in inspect.getmembers(module[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)):
|
|
||||||
self.walker(cls)
|
|
||||||
elif (
|
|
||||||
inspect.isclass(module[1])
|
|
||||||
and module[1].__module__.startswith(self.basename)
|
|
||||||
and module[0] not in self.classes
|
|
||||||
and not module[0].startswith("_")
|
|
||||||
and hasattr(module[1], "__annotations__")
|
|
||||||
):
|
|
||||||
self.this_type = module[0]
|
|
||||||
print("\n\n" + module[0] + "\n")
|
|
||||||
d = {}
|
|
||||||
for param, ptype in typing.get_type_hints(module[1], globalns=None, localns=None).items():
|
|
||||||
if not param.startswith("_") and not param.endswith("_"):
|
|
||||||
print(f"{param} ::: {ptype}")
|
|
||||||
d[param] = self.pytype(ptype)
|
|
||||||
|
|
||||||
if len(d) == 1:
|
|
||||||
key, value = d.popitem()
|
|
||||||
self.classes[module[0]] = value[1]
|
|
||||||
else:
|
|
||||||
self.classes[module[0]] = d
|
|
||||||
|
|
||||||
def run(self, models):
|
|
||||||
self.basename = models.__name__
|
|
||||||
self.walker(("models", models))
|
|
||||||
|
|
||||||
def write(self):
|
|
||||||
with open(self.filename, "w") as file:
|
|
||||||
file.write("declare namespace {} {{\n".format(self.namespace))
|
|
||||||
for cls, params in self.classes.items():
|
|
||||||
if isinstance(params, str):
|
|
||||||
file.write("\ttype {} = {};\n".format(cls, params))
|
|
||||||
else:
|
|
||||||
file.write("\tinterface {} {{\n".format(cls))
|
|
||||||
for name in params:
|
|
||||||
file.write("\t\t{}{}: {};\n".format(name, *params[name]))
|
|
||||||
file.write("\t}\n")
|
|
||||||
file.write("}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def install(arguments):
|
|
||||||
app = create_app()
|
|
||||||
with app.app_context():
|
|
||||||
install_all()
|
|
||||||
|
|
||||||
|
|
||||||
def run(arguments):
|
|
||||||
app = create_app()
|
|
||||||
with app.app_context():
|
|
||||||
app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix=config["FLASCHENGEIST"].get("root", ""))
|
|
||||||
if arguments.debug:
|
|
||||||
environ["FLASK_DEBUG"] = "1"
|
|
||||||
app.run(arguments.host, arguments.port, debug=True)
|
|
||||||
else:
|
|
||||||
app.run(arguments.host, arguments.port, debug=False)
|
|
||||||
|
|
||||||
|
|
||||||
def export(arguments):
|
|
||||||
import flaschengeist.models as models
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
with app.app_context():
|
|
||||||
gen = InterfaceGenerator(arguments.namespace, arguments.file)
|
|
||||||
if not arguments.no_core:
|
|
||||||
gen.run(models)
|
|
||||||
if arguments.plugins is not None:
|
|
||||||
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"):
|
|
||||||
if len(arguments.plugins) == 0 or entry_point.name in arguments.plugins:
|
|
||||||
plg = entry_point.load()
|
|
||||||
if hasattr(plg, "models") and plg.models is not None:
|
|
||||||
gen.run(plg.models)
|
|
||||||
gen.write()
|
|
||||||
|
|
||||||
|
|
||||||
def ldap(arguments):
|
|
||||||
app = create_app()
|
|
||||||
with app.app_context():
|
|
||||||
if arguments.set_admin:
|
|
||||||
from flaschengeist.controller import roleController
|
|
||||||
from flaschengeist.database import db
|
|
||||||
role = roleController.get(arguments.set_admin)
|
|
||||||
role.permissions = roleController.get_permissions()
|
|
||||||
db.session.commit()
|
|
||||||
if arguments.sync:
|
|
||||||
from flaschengeist.controller import userController
|
|
||||||
from flaschengeist.plugins.auth_ldap import AuthLDAP
|
|
||||||
from ldap3 import SUBTREE
|
|
||||||
|
|
||||||
auth_ldap: AuthLDAP = app.config.get("FG_AUTH_BACKEND")
|
|
||||||
if auth_ldap is None or not isinstance(auth_ldap, AuthLDAP):
|
|
||||||
raise Exception("Plugin >auth_ldap< not found")
|
|
||||||
conn = auth_ldap.ldap.connection
|
|
||||||
if not conn:
|
|
||||||
conn = auth_ldap.ldap.connect(auth_ldap.root_dn, auth_ldap.root_secret)
|
|
||||||
conn.search(auth_ldap.search_dn, "(uid=*)", SUBTREE, attributes=["uid", "givenName", "sn", "mail"])
|
|
||||||
ldap_users_response = conn.response
|
|
||||||
for ldap_user in ldap_users_response:
|
|
||||||
uid = ldap_user["attributes"]["uid"][0]
|
|
||||||
userController.find_user(uid)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# create the top-level parser
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
subparsers = parser.add_subparsers(help="sub-command help", dest="sub_command")
|
|
||||||
subparsers.required = True
|
|
||||||
parser_run = subparsers.add_parser("run", help="run flaschengeist")
|
|
||||||
parser_run.set_defaults(func=run)
|
|
||||||
parser_run.add_argument("--host", help="set hostname to listen on", default="127.0.0.1")
|
|
||||||
parser_run.add_argument("--port", help="set port to listen on", type=int, default=5000)
|
|
||||||
parser_run.add_argument("--debug", help="run in debug mode", action="store_true")
|
|
||||||
parser_install = subparsers.add_parser(
|
|
||||||
"install", help="run database setup for flaschengeist and all installed plugins"
|
|
||||||
)
|
|
||||||
parser_install.set_defaults(func=install)
|
|
||||||
parser_export = subparsers.add_parser("export", help="export models to typescript interfaces")
|
|
||||||
parser_export.set_defaults(func=export)
|
|
||||||
parser_export.add_argument("--file", help="Filename where to save", default="flaschengeist.d.ts")
|
|
||||||
parser_export.add_argument("--namespace", help="Namespace of declarations", default="FG")
|
|
||||||
parser_export.add_argument(
|
|
||||||
"--no-core",
|
|
||||||
help="Do not export core declarations (only useful in conjunction with --plugins)",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
parser_export.add_argument("--plugins", help="Also export plugins (none means all)", nargs="*")
|
|
||||||
|
|
||||||
parser_ldap = subparsers.add_parser("ldap", help="LDAP helper utils")
|
|
||||||
parser_ldap.set_defaults(func=ldap)
|
|
||||||
parser_ldap.add_argument('--sync', action="store_true", help="Sync ldap-users with database")
|
|
||||||
parser_ldap.add_argument('--set-admin', type=str, help="Assign all permissions this to group")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
args.func(args)
|
|
42
setup.cfg
42
setup.cfg
|
@ -17,6 +17,48 @@ classifiers =
|
||||||
License :: OSI Approved :: MIT License
|
License :: OSI Approved :: MIT License
|
||||||
Operating System :: OS Independent
|
Operating System :: OS Independent
|
||||||
|
|
||||||
|
[options]
|
||||||
|
include_package_data = True
|
||||||
|
python_requires = >=3.7
|
||||||
|
packages = find:
|
||||||
|
install_requires =
|
||||||
|
Flask >= 2.0
|
||||||
|
Pillow>=8.4.0
|
||||||
|
flask_cors
|
||||||
|
flask_sqlalchemy>=2.5
|
||||||
|
sqlalchemy>=1.4.26
|
||||||
|
toml
|
||||||
|
werkzeug
|
||||||
|
PyMySQL;platform_system=='Windows'
|
||||||
|
mysqlclient;platform_system!='Windows'
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
argon = argon2-cffi
|
||||||
|
ldap = flask_ldapconn; ldap3
|
||||||
|
test = pytest; coverage
|
||||||
|
|
||||||
|
[options.package_data]
|
||||||
|
* = *.toml
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
console_scripts =
|
||||||
|
flaschengeist = flaschengeist.cli:cli
|
||||||
|
flask.commands =
|
||||||
|
ldap = flaschengeist.plugins.auth_ldap.cli:ldap
|
||||||
|
users = flaschengeist.plugins.users.cli:users
|
||||||
|
flaschengeist.plugins =
|
||||||
|
# Authentication providers
|
||||||
|
auth_plain = flaschengeist.plugins.auth_plain:AuthPlain
|
||||||
|
auth_ldap = flaschengeist.plugins.auth_ldap:AuthLDAP [ldap]
|
||||||
|
# Route providers (and misc)
|
||||||
|
auth = flaschengeist.plugins.auth:AuthRoutePlugin
|
||||||
|
users = flaschengeist.plugins.users:UsersPlugin
|
||||||
|
roles = flaschengeist.plugins.roles:RolesPlugin
|
||||||
|
balance = flaschengeist.plugins.balance:BalancePlugin
|
||||||
|
mail = flaschengeist.plugins.message_mail:MailMessagePlugin
|
||||||
|
pricelist = flaschengeist.plugins.pricelist:PriceListPlugin
|
||||||
|
scheduler = flaschengeist.plugins.scheduler:SchedulerPlugin
|
||||||
|
|
||||||
[bdist_wheel]
|
[bdist_wheel]
|
||||||
universal = True
|
universal = True
|
||||||
|
|
||||||
|
|
77
setup.py
77
setup.py
|
@ -1,77 +0,0 @@
|
||||||
from setuptools import setup, find_packages, Command
|
|
||||||
import subprocess
|
|
||||||
import os
|
|
||||||
|
|
||||||
mysql_driver = "PyMySQL" if os.name == "nt" else "mysqlclient"
|
|
||||||
|
|
||||||
|
|
||||||
class DocsCommand(Command):
|
|
||||||
description = "Generate and export API documentation using pdoc3"
|
|
||||||
user_options = [
|
|
||||||
# The format is (long option, short option, description).
|
|
||||||
("output=", "o", "Documentation output path"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.output = "./docs"
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""Run command."""
|
|
||||||
command = [
|
|
||||||
"python",
|
|
||||||
"-m",
|
|
||||||
"pdoc",
|
|
||||||
"--skip-errors",
|
|
||||||
"--html",
|
|
||||||
"--output-dir",
|
|
||||||
self.output,
|
|
||||||
"flaschengeist",
|
|
||||||
]
|
|
||||||
self.announce(
|
|
||||||
"Running command: %s" % str(command),
|
|
||||||
)
|
|
||||||
subprocess.check_call(command)
|
|
||||||
|
|
||||||
|
|
||||||
setup(
|
|
||||||
packages=find_packages(),
|
|
||||||
package_data={"": ["*.toml"]},
|
|
||||||
scripts=["run_flaschengeist"],
|
|
||||||
python_requires=">=3.7",
|
|
||||||
install_requires=[
|
|
||||||
"Flask >= 2.0",
|
|
||||||
"toml",
|
|
||||||
"sqlalchemy>=1.4.26",
|
|
||||||
"flask_sqlalchemy>=2.5",
|
|
||||||
"flask_cors",
|
|
||||||
"Pillow>=8.4.0",
|
|
||||||
"werkzeug",
|
|
||||||
mysql_driver,
|
|
||||||
],
|
|
||||||
extras_require={
|
|
||||||
"ldap": ["flask_ldapconn", "ldap3"],
|
|
||||||
"argon": ["argon2-cffi"],
|
|
||||||
"test": ["pytest", "coverage"],
|
|
||||||
},
|
|
||||||
entry_points={
|
|
||||||
"flaschengeist.plugins": [
|
|
||||||
# Authentication providers
|
|
||||||
"auth_plain = flaschengeist.plugins.auth_plain:AuthPlain",
|
|
||||||
"auth_ldap = flaschengeist.plugins.auth_ldap:AuthLDAP [ldap]",
|
|
||||||
# Route providers (and misc)
|
|
||||||
"auth = flaschengeist.plugins.auth:AuthRoutePlugin",
|
|
||||||
"users = flaschengeist.plugins.users:UsersPlugin",
|
|
||||||
"roles = flaschengeist.plugins.roles:RolesPlugin",
|
|
||||||
"balance = flaschengeist.plugins.balance:BalancePlugin",
|
|
||||||
"mail = flaschengeist.plugins.message_mail:MailMessagePlugin",
|
|
||||||
"pricelist = flaschengeist.plugins.pricelist:PriceListPlugin",
|
|
||||||
"scheduler = flaschengeist.plugins.scheduler:SchedulerPlugin",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
cmdclass={
|
|
||||||
"docs": DocsCommand,
|
|
||||||
},
|
|
||||||
)
|
|
Loading…
Reference in New Issue