From 90e71d40d429adbbad54cc689399ad51bfb09e9e Mon Sep 17 00:00:00 2001 From: Jakub Man Date: Mon, 13 Mar 2023 09:34:43 +0100 Subject: [PATCH 01/43] remove unused app imports --- db-init.py | 2 +- flowapp/auth.py | 22 +++++++++++----------- flowapp/views/api_v1.py | 2 +- flowapp/views/api_v2.py | 2 +- flowapp/views/api_v3.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/db-init.py b/db-init.py index 58ed6b3f..2c8fd680 100644 --- a/db-init.py +++ b/db-init.py @@ -1,6 +1,6 @@ from flask import Flask -from flowapp import app, db +from flowapp import db from flowapp.models import * import config diff --git a/flowapp/auth.py b/flowapp/auth.py index df0e2d38..2aab9d3e 100644 --- a/flowapp/auth.py +++ b/flowapp/auth.py @@ -1,7 +1,7 @@ from functools import wraps -from flask import redirect, request, url_for, session, abort +from flask import current_app, redirect, request, url_for, session, abort -from flowapp import db, app, __version__ +from flowapp import db, __version__ from .models import User @@ -56,8 +56,8 @@ def localhost_only(f): @wraps(f) def decorated(*args, **kwargs): remote = request.remote_addr - localv4 = app.config.get('LOCAL_IP') - localv6 = app.config.get('LOCAL_IP6') + localv4 = current_app.config.get('LOCAL_IP') + localv6 = current_app.config.get('LOCAL_IP6') if remote != localv4 and remote != localv6: print("AUTH LOCAL ONLY FAIL FROM {} / local adresses [{}, {}]".format(remote, localv4, localv6)) abort(403) # Forbidden @@ -89,7 +89,7 @@ def check_auth(uuid): session['app_version'] = __version__ - if app.config.get('SSO_AUTH'): + if current_app.config.get('SSO_AUTH'): # SSO AUTH exist = False if uuid: @@ -97,12 +97,12 @@ def check_auth(uuid): return exist else: # Localhost login / no check - session['user_email'] = app.config['LOCAL_USER_UUID'] - session['user_id'] = app.config['LOCAL_USER_ID'] - session['user_roles'] = app.config['LOCAL_USER_ROLES'] - session['user_orgs'] = ", ".join(org['name'] for org in app.config['LOCAL_USER_ORGS']) - session['user_role_ids'] = app.config['LOCAL_USER_ROLE_IDS'] - session['user_org_ids'] = app.config['LOCAL_USER_ORG_IDS'] + session['user_email'] = current_app.config['LOCAL_USER_UUID'] + session['user_id'] = current_app.config['LOCAL_USER_ID'] + session['user_roles'] = current_app.config['LOCAL_USER_ROLES'] + session['user_orgs'] = ", ".join(org['name'] for org in current_app.config['LOCAL_USER_ORGS']) + session['user_role_ids'] = current_app.config['LOCAL_USER_ROLE_IDS'] + session['user_org_ids'] = current_app.config['LOCAL_USER_ORG_IDS'] roles = [i > 1 for i in session['user_role_ids']] session['can_edit'] = True if all(roles) and roles else [] return True diff --git a/flowapp/views/api_v1.py b/flowapp/views/api_v1.py index 3694557d..1345e602 100644 --- a/flowapp/views/api_v1.py +++ b/flowapp/views/api_v1.py @@ -3,7 +3,7 @@ api = Blueprint("api_v1", __name__, template_folder="templates") -from flowapp import app, db, validators, flowspec, csrf, messages +from flowapp import db, validators, flowspec, csrf, messages @api.route("/auth/", methods=["GET"]) diff --git a/flowapp/views/api_v2.py b/flowapp/views/api_v2.py index e000554e..8767462c 100644 --- a/flowapp/views/api_v2.py +++ b/flowapp/views/api_v2.py @@ -3,7 +3,7 @@ api = Blueprint("api_v2", __name__, template_folder="templates") -from flowapp import app, db, validators, flowspec, csrf, messages +from flowapp import db, validators, flowspec, csrf, messages @api.route("/auth/", methods=["GET"]) diff --git a/flowapp/views/api_v3.py b/flowapp/views/api_v3.py index 4a7b307d..88466db0 100644 --- a/flowapp/views/api_v3.py +++ b/flowapp/views/api_v3.py @@ -3,7 +3,7 @@ api = Blueprint("api_v3", __name__, template_folder="templates") -from flowapp import app, db, validators, flowspec, csrf, messages +from flowapp import db, validators, flowspec, csrf, messages @api.route("/auth", methods=["GET"]) From 00c94c0ed5054a127b22b21569d93adaf9c167eb Mon Sep 17 00:00:00 2001 From: Jakub Man Date: Mon, 13 Mar 2023 09:36:12 +0100 Subject: [PATCH 02/43] replace app variable access outside of init with flask.current_app Using the application context (current_app) helps prevent circular imports when using app context (such as accessing config values) from blueprints or other parts of the application. Furthermore, when creating an app using the factory pattern, the app variable is not available outside of the factory method. For more information see [1]. [1] https://flask.palletsprojects.com/en/2.2.x/appcontext/ --- flowapp/views/api_common.py | 8 ++++---- flowapp/views/api_keys.py | 10 +++++----- flowapp/views/dashboard.py | 3 ++- flowapp/views/rules.py | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/flowapp/views/api_common.py b/flowapp/views/api_common.py index d27f0178..1503bb29 100644 --- a/flowapp/views/api_common.py +++ b/flowapp/views/api_common.py @@ -1,7 +1,7 @@ import jwt import ipaddress -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify, current_app from functools import wraps from datetime import datetime, timedelta @@ -39,7 +39,7 @@ ) -from flowapp import app, db, validators, flowspec, csrf, messages +from flowapp import db, validators, flowspec, csrf, messages def token_required(f): @@ -54,7 +54,7 @@ def decorated(*args, **kwargs): return jsonify({"message": "auth token is missing"}), 401 try: - data = jwt.decode(token, app.config.get("JWT_SECRET"), algorithms=["HS256"]) + data = jwt.decode(token, current_app.config.get("JWT_SECRET"), algorithms=["HS256"]) current_user = data["user"] except jwt.DecodeError: return jsonify({"message": "auth token is invalid"}), 403 @@ -71,7 +71,7 @@ def authorize(user_key): Generate API Key for the loged user using PyJWT :return: page with token """ - jwt_key = app.config.get("JWT_SECRET") + jwt_key = current_app.config.get("JWT_SECRET") model = db.session.query(ApiKey).filter_by(key=user_key).first() print("MODEL", model) diff --git a/flowapp/views/api_keys.py b/flowapp/views/api_keys.py index f066d9be..f97f7774 100644 --- a/flowapp/views/api_keys.py +++ b/flowapp/views/api_keys.py @@ -1,12 +1,12 @@ import jwt -from flask import Blueprint, render_template, redirect, flash, request, url_for, session, make_response +from flask import Blueprint, render_template, redirect, flash, request, url_for, session, make_response, current_app import secrets from ..forms import ApiKeyForm from ..models import ApiKey from ..auth import auth_required -from flowapp import db, app +from flowapp import db COOKIE_KEY = 'keylist' @@ -20,14 +20,14 @@ def all(): Show user api keys :return: page with keys """ - jwt_key = app.config.get('JWT_SECRET') + jwt_key = current_app.config.get('JWT_SECRET') keys = db.session.query(ApiKey).filter_by(user_id=session['user_id']).all() payload = {'keys': [key.id for key in keys]} encoded = jwt.encode(payload, jwt_key, algorithm='HS256') resp = make_response(render_template('pages/api_key.j2', keys=keys)) - if app.config.get('DEVEL'): + if current_app.config.get('DEVEL'): resp.set_cookie(COOKIE_KEY, encoded, httponly=True, samesite='Lax') else: resp.set_cookie(COOKIE_KEY, encoded, secure=True, httponly=True, samesite='Lax') @@ -76,7 +76,7 @@ def delete(key_id): :param key_id: integer """ key_list = request.cookies.get(COOKIE_KEY) - key_list = jwt.decode(key_list, app.config.get('JWT_SECRET'), algorithms=['HS256']) + key_list = jwt.decode(key_list, current_app.config.get('JWT_SECRET'), algorithms=['HS256']) model = db.session.query(ApiKey).get(key_id) if model.id not in key_list['keys']: diff --git a/flowapp/views/dashboard.py b/flowapp/views/dashboard.py index 68e46aa1..916971b9 100644 --- a/flowapp/views/dashboard.py +++ b/flowapp/views/dashboard.py @@ -2,7 +2,8 @@ from datetime import datetime from flask import Blueprint, render_template, request, session, make_response, abort -from flowapp import auth_required, constants, models, app, validators, flowspec +from flowapp import constants, models, validators, flowspec +from flowapp.auth import auth_required from flowapp.constants import RULE_TYPE_DISPATCH, SORT_ARG, ORDER_ARG, DEFAULT_ORDER, DEFAULT_SORT, RULE_TYPES, \ SEARCH_ARG, RULE_ARG, TYPE_ARG, RULES_KEY, ORDSRC_ARG, COLSPANS, COMP_FUNCS, COUNT_MATCH from flowapp.utils import active_css_rstate, other_rtypes diff --git a/flowapp/views/rules.py b/flowapp/views/rules.py index 3e039c93..155af61a 100644 --- a/flowapp/views/rules.py +++ b/flowapp/views/rules.py @@ -7,7 +7,7 @@ from flask import (Blueprint, flash, redirect, render_template, request, session, url_for) -from flowapp import app, constants, db, messages +from flowapp import constants, db, messages from flowapp.auth import (admin_required, auth_required, localhost_only, user_or_admin_required) from flowapp.forms import IPv4Form, IPv6Form, RTBHForm From b85c18c3e66de07d3a9fdc627d0d24948ada8662 Mon Sep 17 00:00:00 2001 From: Jakub Man Date: Mon, 13 Mar 2023 09:46:01 +0100 Subject: [PATCH 03/43] use factory pattern for app initialization The factory pattern allows more control for when the application is created. It also enables more flexibility for the server software used to run the application. Server softwares (such as gunicorn) can now create ExaFS instances on demand and dynamically scale the application. --- flowapp/__init__.py | 239 ++++++++++++++++++++------------------ flowapp/tests/conftest.py | 6 +- run.py | 6 +- 3 files changed, 132 insertions(+), 119 deletions(-) diff --git a/flowapp/__init__.py b/flowapp/__init__.py index dce4bf6f..32678caf 100644 --- a/flowapp/__init__.py +++ b/flowapp/__init__.py @@ -9,144 +9,151 @@ __version__ = '0.7.0' -app = Flask(__name__) db = SQLAlchemy() -migrate = Migrate(app, db) -csrf = CSRFProtect(app) - -# Map SSO attributes from ADFS to session keys under session['user'] -#: Default attribute map -SSO_ATTRIBUTE_MAP = { - 'eppn': (True, 'eppn'), - 'cn': (False, 'cn'), -} - -app.config.setdefault('VERSION', __version__) -app.config.setdefault('SSO_ATTRIBUTE_MAP', SSO_ATTRIBUTE_MAP) -app.config.setdefault('SSO_LOGIN_URL', '/login') - -# This attaches the *flask_sso* login handler to the SSO_LOGIN_URL, -ext = SSO(app=app) - -from flowapp import models, constants, validators -from .views.admin import admin -from .views.rules import rules -from .views.api_v1 import api as api_v1 -from .views.api_v2 import api as api_v2 -from .views.api_v3 import api as api_v3 -from .views.api_keys import api_keys -from .auth import auth_required -from .views.dashboard import dashboard - -# no need for csrf on api because we use JWT -csrf.exempt(api_v1) -csrf.exempt(api_v2) -csrf.exempt(api_v3) - -app.register_blueprint(admin, url_prefix='/admin') -app.register_blueprint(rules, url_prefix='/rules') -app.register_blueprint(api_keys, url_prefix='/api_keys') -app.register_blueprint(api_v1, url_prefix='/api/v1') -app.register_blueprint(api_v2, url_prefix='/api/v2') -app.register_blueprint(api_v3, url_prefix='/api/v3') -app.register_blueprint(dashboard, url_prefix='/dashboard') - - -@ext.login_handler -def login(user_info): - try: - uuid = user_info.get('eppn') - except KeyError: - uuid = False - return redirect('/') - else: - user = db.session.query(models.User).filter_by(uuid=uuid).first() +migrate = Migrate() +csrf = CSRFProtect() + +def create_app(): + app = Flask(__name__) + # Map SSO attributes from ADFS to session keys under session['user'] + #: Default attribute map + SSO_ATTRIBUTE_MAP = { + 'eppn': (True, 'eppn'), + 'cn': (False, 'cn'), + } + + # db.init_app(app) + migrate.init_app(app, db) + csrf.init_app(app) + + app.config.setdefault('VERSION', __version__) + app.config.setdefault('SSO_ATTRIBUTE_MAP', SSO_ATTRIBUTE_MAP) + app.config.setdefault('SSO_LOGIN_URL', '/login') + + # This attaches the *flask_sso* login handler to the SSO_LOGIN_URL, + ext = SSO(app=app) + + from flowapp import models, constants, validators + from .views.admin import admin + from .views.rules import rules + from .views.api_v1 import api as api_v1 + from .views.api_v2 import api as api_v2 + from .views.api_v3 import api as api_v3 + from .views.api_keys import api_keys + from .auth import auth_required + from .views.dashboard import dashboard + + # no need for csrf on api because we use JWT + csrf.exempt(api_v1) + csrf.exempt(api_v2) + csrf.exempt(api_v3) + + app.register_blueprint(admin, url_prefix='/admin') + app.register_blueprint(rules, url_prefix='/rules') + app.register_blueprint(api_keys, url_prefix='/api_keys') + app.register_blueprint(api_v1, url_prefix='/api/v1') + app.register_blueprint(api_v2, url_prefix='/api/v2') + app.register_blueprint(api_v3, url_prefix='/api/v3') + app.register_blueprint(dashboard, url_prefix='/dashboard') + + + @ext.login_handler + def login(user_info): try: - session['user_uuid'] = user.uuid - session['user_email'] = user.uuid - session['user_name'] = user.name - session['user_id'] = user.id - session['user_roles'] = [role.name for role in user.role.all()] - session['user_orgs'] = ", ".join(org.name for org in user.organization.all()) - session['user_role_ids'] = [role.id for role in user.role.all()] - session['user_org_ids'] = [org.id for org in user.organization.all()] - roles = [i > 1 for i in session['user_role_ids']] - session['can_edit'] = True if all(roles) and roles else [] - except AttributeError: + uuid = user_info.get('eppn') + except KeyError: + uuid = False + return redirect('/') + else: + user = db.session.query(models.User).filter_by(uuid=uuid).first() + try: + session['user_uuid'] = user.uuid + session['user_email'] = user.uuid + session['user_name'] = user.name + session['user_id'] = user.id + session['user_roles'] = [role.name for role in user.role.all()] + session['user_orgs'] = ", ".join(org.name for org in user.organization.all()) + session['user_role_ids'] = [role.id for role in user.role.all()] + session['user_org_ids'] = [org.id for org in user.organization.all()] + roles = [i > 1 for i in session['user_role_ids']] + session['can_edit'] = True if all(roles) and roles else [] + except AttributeError: + return redirect('/') + return redirect('/') - return redirect('/') + @app.route('/logout') + def logout(): + session['user_uuid'] = False + session['user_id'] = False + session.clear() + return redirect(app.config.get('LOGOUT_URL')) -@app.route('/logout') -def logout(): - session['user_uuid'] = False - session['user_id'] = False - session.clear() - return redirect(app.config.get('LOGOUT_URL')) + @app.route('/') + @auth_required + def index(): + try: + rtype = session[constants.TYPE_ARG] + except KeyError: + rtype = 'ipv4' -@app.route('/') -@auth_required -def index(): - try: - rtype = session[constants.TYPE_ARG] - except KeyError: - rtype = 'ipv4' + try: + rstate = session[constants.RULE_ARG] + except KeyError: + rstate = 'active' - try: - rstate = session[constants.RULE_ARG] - except KeyError: - rstate = 'active' + try: + sorter = session[constants.SORT_ARG] + except KeyError: + sorter = constants.DEFAULT_SORT - try: - sorter = session[constants.SORT_ARG] - except KeyError: - sorter = constants.DEFAULT_SORT + try: + orderer = session[constants.ORDER_ARG] + except KeyError: + orderer = constants.DEFAULT_ORDER - try: - orderer = session[constants.ORDER_ARG] - except KeyError: - orderer = constants.DEFAULT_ORDER + return redirect(url_for('dashboard.index', + rtype=rtype, + rstate=rstate, + sort=sorter, + order=orderer)) - return redirect(url_for('dashboard.index', - rtype=rtype, - rstate=rstate, - sort=sorter, - order=orderer)) + @app.teardown_appcontext + def shutdown_session(exception=None): + db.session.remove() -@app.teardown_appcontext -def shutdown_session(exception=None): - db.session.remove() + # HTTP error handling + @app.errorhandler(404) + def not_found(error): + return render_template('errors/404.j2'), 404 -# HTTP error handling -@app.errorhandler(404) -def not_found(error): - return render_template('errors/404.j2'), 404 + @app.errorhandler(500) + def internal_error(exception): + app.logger.error(exception) + return render_template('errors/500.j2'), 500 -@app.errorhandler(500) -def internal_error(exception): - app.logger.error(exception) - return render_template('errors/500.j2'), 500 + @app.context_processor + def utility_processor(): + def editable_rule(rule): + if rule: + validators.editable_range(rule, models.get_user_nets(session['user_id'])) + return True + return False -@app.context_processor -def utility_processor(): - def editable_rule(rule): - if rule: - validators.editable_range(rule, models.get_user_nets(session['user_id'])) - return True - return False + return dict(editable_rule=editable_rule) - return dict(editable_rule=editable_rule) + @app.template_filter('strftime') + def format_datetime(value): + format = "y/MM/dd HH:mm" -@app.template_filter('strftime') -def format_datetime(value): - format = "y/MM/dd HH:mm" + return babel.dates.format_datetime(value, format) - return babel.dates.format_datetime(value, format) + return app diff --git a/flowapp/tests/conftest.py b/flowapp/tests/conftest.py index 48014ec1..14c2047b 100644 --- a/flowapp/tests/conftest.py +++ b/flowapp/tests/conftest.py @@ -7,7 +7,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from flowapp import app as _app +from flowapp import create_app from flowapp import db as _db import flowapp.models @@ -53,6 +53,8 @@ def app(request): Create a Flask app, and override settings, for the whole test session. """ + _app = create_app() + _app.config.update( EXA_API = 'HTTP', EXA_API_URL = 'http://localhost:5000/', @@ -102,7 +104,7 @@ def db(app, request): """ Create entire database for every test. """ - engine = create_engine(_app.config['SQLALCHEMY_DATABASE_URI'], echo=True) + engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI'], echo=True) session_factory = sessionmaker(bind=engine) print('\n----- CREATE TEST DB CONNECTION POOL\n') if os.path.exists(TESTDB_PATH): diff --git a/run.py b/run.py index bd6dae9b..106f0d2e 100644 --- a/run.py +++ b/run.py @@ -1,8 +1,12 @@ from os import environ -from flowapp import app, db +from flowapp import create_app, db import config + +# Call app factory +app = create_app() + # Configurations try: env = environ['EXAFS_ENV'] From 23af19b436eed71d8296246e4e5d07e155f58291 Mon Sep 17 00:00:00 2001 From: Jakub Man Date: Wed, 1 Mar 2023 10:52:42 +0100 Subject: [PATCH 04/43] fix crash when EXA_API key is not set in config The GUI would crash on KeyError when trying to add a new rule, unless the 'EXA_API' key was set in the config. Since the key is not in the example config, just renaming the example config would lead to the GUI not working. This commit fixes the issue. Fixes: 0e45cd0 ("v 0.7.0 - two options for ExaAPI") --- flowapp/output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flowapp/output.py b/flowapp/output.py index e4ec8036..b6846eef 100644 --- a/flowapp/output.py +++ b/flowapp/output.py @@ -19,7 +19,7 @@ def announce_route(route): API must be set in app config.py defaults to HTTP API """ - if current_app.config['EXA_API'] == 'RABBIT': + if current_app.config.get('EXA_API') == 'RABBIT': announce_to_rabbitmq(route) else: announce_to_http(route) From 4ffec0a84810a2598c3f8408ea47ab10af320654 Mon Sep 17 00:00:00 2001 From: Jakub Man Date: Wed, 1 Mar 2023 14:46:47 +0100 Subject: [PATCH 05/43] fix exception when removing multiple RTBH rules The group_delete method inserted 'rtbh' as rule_type attribute for the Log table, however the rule_type attribute is of type int. This commit fixes the issue by inserting the numeric representation of the RTBH rule type when creating records in the Log table. --- flowapp/views/rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flowapp/views/rules.py b/flowapp/views/rules.py index 3e039c93..314d951b 100644 --- a/flowapp/views/rules.py +++ b/flowapp/views/rules.py @@ -195,7 +195,7 @@ def group_delete(): route = route_model(model, constants.WITHDRAW) announce_route(route) - log_withdraw(session['user_id'], route, rule_type, model.id, "{} / {}".format(session['user_email'], session['user_orgs'])) + log_withdraw(session['user_id'], route, rule_type_int, model.id, "{} / {}".format(session['user_email'], session['user_orgs'])) db.session.query(model_name).filter(model_name.id.in_(to_delete)).delete(synchronize_session=False) db.session.commit() From 95106e49338bc5167b2a927d12e9d75f7bc7d3ad Mon Sep 17 00:00:00 2001 From: Jakub Man Date: Fri, 3 Mar 2023 14:40:22 +0100 Subject: [PATCH 06/43] fix wrong error message formatting in _is_subnet_of validator The previous formatting would lead to an error "str is not callable", instead of formatting properly. This commit fixes the issue. --- flowapp/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flowapp/validators.py b/flowapp/validators.py index de8a0105..49ea2db8 100644 --- a/flowapp/validators.py +++ b/flowapp/validators.py @@ -336,7 +336,7 @@ def _is_subnet_of(a, b): try: # Always false if one is v4 and the other is v6. if a._version != b._version: - raise TypeError("%s and %s are not of the same version"(a, b)) + raise TypeError("%s and %s are not of the same version" % (a, b)) return (b.network_address <= a.network_address and b.broadcast_address >= a.broadcast_address) except AttributeError: From 72d396fb32027b0b036862195daced3311de9c59 Mon Sep 17 00:00:00 2001 From: Jakub Man Date: Mon, 13 Mar 2023 10:39:03 +0100 Subject: [PATCH 07/43] setuptools configuration The setup.py file allows the application to be packaged for easier distribution. --- MANIFEST.in | 8 ++++++++ setup.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100755 MANIFEST.in create mode 100755 setup.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100755 index 00000000..1f4f66ac --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include flowapp/static/* +include flowapp/static/js/* + +include flowapp/templates/* +include flowapp/templates/errors/* +include flowapp/templates/forms/* +include flowapp/templates/layouts/* +include flowapp/templates/pages/* diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..2d4cdc42 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +""" +Author(s): Jakub Man + +Setuptools configuration +""" + +import setuptools +from flowapp import __version__ + +setuptools.setup( + name="exafs", + version=__version__, + author="CESNET / Jiri Vrany, Petr Adamec, Josef Verich, Jakub Man", + description="Tool for creation, validation, and execution of ExaBGP messages.", + url="https://github.com/CESNET/exafs", + license="MIT", + py_modules=["flowapp", "exaapi"], + packages=setuptools.find_packages(), + include_package_data=True, + python_requires=">=3.8", + install_requires=[ + "Flask>=2.0.2", + "Flask-SQLAlchemy<3.0.0", + "Flask-SSO>=0.4.0", + "Flask-WTF>=1.0.0", + "Flask-Migrate>=3.0.0", + "Flask-Script>=2.0.0", + "PyJWT>=2.4.0", + "PyMySQL>=1.0.0", + "pytest>=7.0.0", + "requests>=2.20.0", + "babel>=2.7.0", + "mysqlclient>=2.0.0", + "email_validator>=1.1", + "pika>=1.3.0", + ], +) From c3269a45a0417641be808428d26ca43b9b3af52d Mon Sep 17 00:00:00 2001 From: Jiri Vrany Date: Mon, 13 Mar 2023 14:41:32 +0100 Subject: [PATCH 08/43] v 0.7.1 - app factory, minor bug changes, setup.py prepared --- flowapp/__init__.py | 103 ++++++------ flowapp/views/admin.py | 365 ++++++++++++++++++++++++----------------- run.py | 5 +- setup.cfg | 2 + 4 files changed, 272 insertions(+), 203 deletions(-) create mode 100644 setup.cfg diff --git a/flowapp/__init__.py b/flowapp/__init__.py index 32678caf..081326ec 100644 --- a/flowapp/__init__.py +++ b/flowapp/__init__.py @@ -7,29 +7,30 @@ from flask_wtf.csrf import CSRFProtect from flask_migrate import Migrate -__version__ = '0.7.0' +__version__ = "0.7.1" db = SQLAlchemy() migrate = Migrate() csrf = CSRFProtect() + def create_app(): app = Flask(__name__) # Map SSO attributes from ADFS to session keys under session['user'] #: Default attribute map SSO_ATTRIBUTE_MAP = { - 'eppn': (True, 'eppn'), - 'cn': (False, 'cn'), + "eppn": (True, "eppn"), + "cn": (False, "cn"), } # db.init_app(app) migrate.init_app(app, db) csrf.init_app(app) - app.config.setdefault('VERSION', __version__) - app.config.setdefault('SSO_ATTRIBUTE_MAP', SSO_ATTRIBUTE_MAP) - app.config.setdefault('SSO_LOGIN_URL', '/login') + app.config.setdefault("VERSION", __version__) + app.config.setdefault("SSO_ATTRIBUTE_MAP", SSO_ATTRIBUTE_MAP) + app.config.setdefault("SSO_LOGIN_URL", "/login") # This attaches the *flask_sso* login handler to the SSO_LOGIN_URL, ext = SSO(app=app) @@ -49,61 +50,60 @@ def create_app(): csrf.exempt(api_v2) csrf.exempt(api_v3) - app.register_blueprint(admin, url_prefix='/admin') - app.register_blueprint(rules, url_prefix='/rules') - app.register_blueprint(api_keys, url_prefix='/api_keys') - app.register_blueprint(api_v1, url_prefix='/api/v1') - app.register_blueprint(api_v2, url_prefix='/api/v2') - app.register_blueprint(api_v3, url_prefix='/api/v3') - app.register_blueprint(dashboard, url_prefix='/dashboard') - + app.register_blueprint(admin, url_prefix="/admin") + app.register_blueprint(rules, url_prefix="/rules") + app.register_blueprint(api_keys, url_prefix="/api_keys") + app.register_blueprint(api_v1, url_prefix="/api/v1") + app.register_blueprint(api_v2, url_prefix="/api/v2") + app.register_blueprint(api_v3, url_prefix="/api/v3") + app.register_blueprint(dashboard, url_prefix="/dashboard") @ext.login_handler def login(user_info): try: - uuid = user_info.get('eppn') + uuid = user_info.get("eppn") except KeyError: uuid = False - return redirect('/') + return redirect("/") else: user = db.session.query(models.User).filter_by(uuid=uuid).first() try: - session['user_uuid'] = user.uuid - session['user_email'] = user.uuid - session['user_name'] = user.name - session['user_id'] = user.id - session['user_roles'] = [role.name for role in user.role.all()] - session['user_orgs'] = ", ".join(org.name for org in user.organization.all()) - session['user_role_ids'] = [role.id for role in user.role.all()] - session['user_org_ids'] = [org.id for org in user.organization.all()] - roles = [i > 1 for i in session['user_role_ids']] - session['can_edit'] = True if all(roles) and roles else [] + session["user_uuid"] = user.uuid + session["user_email"] = user.uuid + session["user_name"] = user.name + session["user_id"] = user.id + session["user_roles"] = [role.name for role in user.role.all()] + session["user_orgs"] = ", ".join( + org.name for org in user.organization.all() + ) + session["user_role_ids"] = [role.id for role in user.role.all()] + session["user_org_ids"] = [org.id for org in user.organization.all()] + roles = [i > 1 for i in session["user_role_ids"]] + session["can_edit"] = True if all(roles) and roles else [] except AttributeError: - return redirect('/') - - return redirect('/') + return redirect("/") + return redirect("/") - @app.route('/logout') + @app.route("/logout") def logout(): - session['user_uuid'] = False - session['user_id'] = False + session["user_uuid"] = False + session["user_id"] = False session.clear() - return redirect(app.config.get('LOGOUT_URL')) + return redirect(app.config.get("LOGOUT_URL")) - - @app.route('/') + @app.route("/") @auth_required def index(): try: rtype = session[constants.TYPE_ARG] except KeyError: - rtype = 'ipv4' + rtype = "ipv4" try: rstate = session[constants.RULE_ARG] except KeyError: - rstate = 'active' + rstate = "active" try: sorter = session[constants.SORT_ARG] @@ -115,42 +115,43 @@ def index(): except KeyError: orderer = constants.DEFAULT_ORDER - return redirect(url_for('dashboard.index', - rtype=rtype, - rstate=rstate, - sort=sorter, - order=orderer)) - + return redirect( + url_for( + "dashboard.index", + rtype=rtype, + rstate=rstate, + sort=sorter, + order=orderer, + ) + ) @app.teardown_appcontext def shutdown_session(exception=None): db.session.remove() - # HTTP error handling @app.errorhandler(404) def not_found(error): - return render_template('errors/404.j2'), 404 - + return render_template("errors/404.j2"), 404 @app.errorhandler(500) def internal_error(exception): app.logger.error(exception) - return render_template('errors/500.j2'), 500 - + return render_template("errors/500.j2"), 500 @app.context_processor def utility_processor(): def editable_rule(rule): if rule: - validators.editable_range(rule, models.get_user_nets(session['user_id'])) + validators.editable_range( + rule, models.get_user_nets(session["user_id"]) + ) return True return False return dict(editable_rule=editable_rule) - - @app.template_filter('strftime') + @app.template_filter("strftime") def format_datetime(value): format = "y/MM/dd HH:mm" diff --git a/flowapp/views/admin.py b/flowapp/views/admin.py index c6646027..16aaebdc 100644 --- a/flowapp/views/admin.py +++ b/flowapp/views/admin.py @@ -5,16 +5,26 @@ from sqlalchemy.exc import IntegrityError from ..forms import ASPathForm, UserForm, ActionForm, OrganizationForm, CommunityForm -from ..models import ASPath, User, Action, Organization, Role, insert_user, get_existing_action, Community, \ - get_existing_community, Log +from ..models import ( + ASPath, + User, + Action, + Organization, + Role, + insert_user, + get_existing_action, + Community, + get_existing_community, + Log, +) from ..auth import auth_required, admin_required from flowapp import db -admin = Blueprint('admin', __name__, template_folder='templates') +admin = Blueprint("admin", __name__, template_folder="templates") -@admin.route('/log', methods=['GET'], defaults={"page": 1}) -@admin.route('/log/', methods=['GET']) +@admin.route("/log", methods=["GET"], defaults={"page": 1}) +@admin.route("/log/", methods=["GET"]) @auth_required @admin_required def log(page): @@ -24,21 +34,27 @@ def log(page): per_page = 20 now = datetime.now() week_ago = now - timedelta(weeks=1) - logs = Log.query.order_by(Log.time.desc()).filter(Log.time > week_ago).paginate(page,per_page,error_out=False) - return render_template('pages/logs.j2', logs=logs) + logs = ( + Log.query.order_by(Log.time.desc()) + .filter(Log.time > week_ago) + .paginate(page=page, per_page=per_page, max_per_page=None, error_out=False) + ) + return render_template("pages/logs.j2", logs=logs) -@admin.route('/user', methods=['GET', 'POST']) +@admin.route("/user", methods=["GET", "POST"]) @auth_required @admin_required def user(): form = UserForm(request.form) - form.role_ids.choices = [(g.id, g.name) - for g in db.session.query(Role).order_by('name')] - form.org_ids.choices = [(g.id, g.name) - for g in db.session.query(Organization).order_by('name')] - - if request.method == 'POST' and form.validate(): + form.role_ids.choices = [ + (g.id, g.name) for g in db.session.query(Role).order_by("name") + ] + form.org_ids.choices = [ + (g.id, g.name) for g in db.session.query(Organization).order_by("name") + ] + + if request.method == "POST" and form.validate(): # test if user is unique exist = db.session.query(User).filter_by(uuid=form.uuid.data).first() if not exist: @@ -49,41 +65,52 @@ def user(): email=form.email.data, comment=form.comment.data, role_ids=form.role_ids.data, - org_ids=form.org_ids.data) - flash('User saved') - return redirect(url_for('admin.users')) + org_ids=form.org_ids.data, + ) + flash("User saved") + return redirect(url_for("admin.users")) else: - flash(u'User {} already exists'.format( - form.email.data), 'alert-danger') + flash("User {} already exists".format(form.email.data), "alert-danger") - action_url = url_for('admin.user') - return render_template('forms/simple_form.j2', title="Add new user to Flowspec", form=form, action_url=action_url) + action_url = url_for("admin.user") + return render_template( + "forms/simple_form.j2", + title="Add new user to Flowspec", + form=form, + action_url=action_url, + ) -@admin.route('/user/edit/', methods=['GET', 'POST']) +@admin.route("/user/edit/", methods=["GET", "POST"]) @auth_required @admin_required def edit_user(user_id): user = db.session.query(User).get(user_id) form = UserForm(request.form, obj=user) - form.role_ids.choices = [(g.id, g.name) - for g in db.session.query(Role).order_by('name')] - form.org_ids.choices = [(g.id, g.name) - for g in db.session.query(Organization).order_by('name')] - - if request.method == 'POST' and form.validate(): + form.role_ids.choices = [ + (g.id, g.name) for g in db.session.query(Role).order_by("name") + ] + form.org_ids.choices = [ + (g.id, g.name) for g in db.session.query(Organization).order_by("name") + ] + + if request.method == "POST" and form.validate(): user.update(form) - return redirect(url_for('admin.users')) + return redirect(url_for("admin.users")) form.role_ids.data = [role.id for role in user.role] form.org_ids.data = [org.id for org in user.organization] - action_url = url_for('admin.edit_user', user_id=user_id) + action_url = url_for("admin.edit_user", user_id=user_id) - return render_template('forms/simple_form.j2', title=u"Editing {}".format(user.email), form=form, - action_url=action_url) + return render_template( + "forms/simple_form.j2", + title="Editing {}".format(user.email), + form=form, + action_url=action_url, + ) -@admin.route('/user/delete/', methods=['GET']) +@admin.route("/user/delete/", methods=["GET"]) @auth_required @admin_required def delete_user(user_id): @@ -91,216 +118,247 @@ def delete_user(user_id): username = user.email db.session.delete(user) - message = u'User {} deleted'.format(username) - alert_type = 'alert-success' + message = "User {} deleted".format(username) + alert_type = "alert-success" try: db.session.commit() except IntegrityError as e: - message = u'User {} owns some rules, can not be deleted!'.format(username) - alert_type = 'alert-danger' + message = "User {} owns some rules, can not be deleted!".format(username) + alert_type = "alert-danger" print(e) flash(message, alert_type) - return redirect(url_for('admin.users')) + return redirect(url_for("admin.users")) -@admin.route('/users') +@admin.route("/users") @auth_required @admin_required def users(): users = User.query.all() - return render_template('pages/users.j2', users=users) + return render_template("pages/users.j2", users=users) -@admin.route('/organizations') +@admin.route("/organizations") @auth_required @admin_required def organizations(): orgs = db.session.query(Organization).all() - return render_template('pages/orgs.j2', orgs=orgs) + return render_template("pages/orgs.j2", orgs=orgs) -@admin.route('/organization', methods=['GET', 'POST']) +@admin.route("/organization", methods=["GET", "POST"]) @auth_required @admin_required def organization(): form = OrganizationForm(request.form) - if request.method == 'POST' and form.validate(): + if request.method == "POST" and form.validate(): # test if user is unique exist = db.session.query(Organization).filter_by(name=form.name.data).first() if not exist: org = Organization(name=form.name.data, arange=form.arange.data) db.session.add(org) db.session.commit() - flash('Organization saved') - return redirect(url_for('admin.organizations')) + flash("Organization saved") + return redirect(url_for("admin.organizations")) else: - flash(u'Organization {} already exists'.format( - form.name.data), 'alert-danger') + flash( + "Organization {} already exists".format(form.name.data), "alert-danger" + ) - action_url = url_for('admin.organization') - return render_template('forms/simple_form.j2', title="Add new organization to Flowspec", form=form, - action_url=action_url) + action_url = url_for("admin.organization") + return render_template( + "forms/simple_form.j2", + title="Add new organization to Flowspec", + form=form, + action_url=action_url, + ) -@admin.route('/organization/edit/', methods=['GET', 'POST']) +@admin.route("/organization/edit/", methods=["GET", "POST"]) @auth_required @admin_required def edit_organization(org_id): org = db.session.query(Organization).get(org_id) form = OrganizationForm(request.form, obj=org) - if request.method == 'POST' and form.validate(): + if request.method == "POST" and form.validate(): form.populate_obj(org) db.session.commit() - flash('Organization updated') - return redirect(url_for('admin.organizations')) + flash("Organization updated") + return redirect(url_for("admin.organizations")) - action_url = url_for('admin.edit_organization', org_id=org.id) - return render_template('forms/simple_form.j2', title=u"Editing {}".format(org.name), form=form, - action_url=action_url) + action_url = url_for("admin.edit_organization", org_id=org.id) + return render_template( + "forms/simple_form.j2", + title="Editing {}".format(org.name), + form=form, + action_url=action_url, + ) -@admin.route('/organization/delete/', methods=['GET']) +@admin.route("/organization/delete/", methods=["GET"]) @auth_required @admin_required def delete_organization(org_id): org = db.session.query(Organization).get(org_id) aname = org.name db.session.delete(org) - message = u'Organization {} deleted'.format(aname) - alert_type = 'alert-success' + message = "Organization {} deleted".format(aname) + alert_type = "alert-success" try: db.session.commit() except IntegrityError: - message = u'Organization {} has some users, can not be deleted!'.format(aname) - alert_type = 'alert-danger' + message = "Organization {} has some users, can not be deleted!".format(aname) + alert_type = "alert-danger" flash(message, alert_type) db.session.commit() - return redirect(url_for('admin.organizations')) + return redirect(url_for("admin.organizations")) -@admin.route('/as-paths') +@admin.route("/as-paths") @auth_required @admin_required def as_paths(): mpaths = db.session.query(ASPath).all() - return render_template('pages/as_paths.j2', paths=mpaths) + return render_template("pages/as_paths.j2", paths=mpaths) -@admin.route('/as-path', methods=['GET', 'POST']) +@admin.route("/as-path", methods=["GET", "POST"]) @auth_required @admin_required def as_path(): form = ASPathForm(request.form) - if request.method == 'POST' and form.validate(): + if request.method == "POST" and form.validate(): # test if user is unique exist = db.session.query(ASPath).filter_by(prefix=form.prefix.data).first() if not exist: pth = ASPath(prefix=form.prefix.data, as_path=form.as_path.data) db.session.add(pth) db.session.commit() - flash('AS-path saved') - return redirect(url_for('admin.as_paths')) + flash("AS-path saved") + return redirect(url_for("admin.as_paths")) else: - flash(u'Prefix {} already taken'.format( - form.prefix.data), 'alert-danger') + flash("Prefix {} already taken".format(form.prefix.data), "alert-danger") - action_url = url_for('admin.as_path') - return render_template('forms/simple_form.j2', title="Add new AS-path to Flowspec", form=form, - action_url=action_url) + action_url = url_for("admin.as_path") + return render_template( + "forms/simple_form.j2", + title="Add new AS-path to Flowspec", + form=form, + action_url=action_url, + ) -@admin.route('/as-path/edit/', methods=['GET', 'POST']) +@admin.route("/as-path/edit/", methods=["GET", "POST"]) @auth_required @admin_required def edit_as_path(path_id): pth = db.session.query(ASPath).get(path_id) form = ASPathForm(request.form, obj=pth) - if request.method == 'POST' and form.validate(): + if request.method == "POST" and form.validate(): form.populate_obj(pth) db.session.commit() - flash('AS-path updated') - return redirect(url_for('admin.as_paths')) + flash("AS-path updated") + return redirect(url_for("admin.as_paths")) - action_url = url_for('admin.edit_as_path', path_id=pth.id) - return render_template('forms/simple_form.j2', title=u"Editing {}".format(pth.prefix), form=form, - action_url=action_url) + action_url = url_for("admin.edit_as_path", path_id=pth.id) + return render_template( + "forms/simple_form.j2", + title="Editing {}".format(pth.prefix), + form=form, + action_url=action_url, + ) -@admin.route('/as-path/delete/', methods=['GET']) +@admin.route("/as-path/delete/", methods=["GET"]) @auth_required @admin_required def delete_as_path(path_id): pth = db.session.query(ASPath).get(path_id) db.session.delete(pth) - message = f'AS path {pth.prefix} : {pth.as_path} deleted' - alert_type = 'alert-success' - + message = f"AS path {pth.prefix} : {pth.as_path} deleted" + alert_type = "alert-success" + flash(message, alert_type) db.session.commit() - return redirect(url_for('admin.as_paths')) + return redirect(url_for("admin.as_paths")) -@admin.route('/actions') +@admin.route("/actions") @auth_required @admin_required def actions(): actions = db.session.query(Action).all() - return render_template('pages/actions.j2', actions=actions) + return render_template("pages/actions.j2", actions=actions) -@admin.route('/action', methods=['GET', 'POST']) +@admin.route("/action", methods=["GET", "POST"]) @auth_required @admin_required def action(): form = ActionForm(request.form) - if request.method == 'POST' and form.validate(): + if request.method == "POST" and form.validate(): # test if Acttion is unique exist = get_existing_action(form.name.data, form.command.data) if not exist: - action = Action(name=form.name.data, - command=form.command.data, - description=form.description.data, - role_id=form.role_id.data) + action = Action( + name=form.name.data, + command=form.command.data, + description=form.description.data, + role_id=form.role_id.data, + ) db.session.add(action) db.session.commit() - flash('Action saved', 'alert-success') - return redirect(url_for('admin.actions')) + flash("Action saved", "alert-success") + return redirect(url_for("admin.actions")) else: - flash(u'Action with name {} or command {} already exists'.format( - form.name.data, form.command.data), 'alert-danger') - - action_url = url_for('admin.action') - return render_template('forms/simple_form.j2', title="Add new action to Flowspec", form=form, action_url=action_url) - - -@admin.route('/action/edit/', methods=['GET', 'POST']) + flash( + "Action with name {} or command {} already exists".format( + form.name.data, form.command.data + ), + "alert-danger", + ) + + action_url = url_for("admin.action") + return render_template( + "forms/simple_form.j2", + title="Add new action to Flowspec", + form=form, + action_url=action_url, + ) + + +@admin.route("/action/edit/", methods=["GET", "POST"]) @auth_required @admin_required def edit_action(action_id): action = db.session.query(Action).get(action_id) print(action.role_id) form = ActionForm(request.form, obj=action) - if request.method == 'POST' and form.validate(): + if request.method == "POST" and form.validate(): form.populate_obj(action) db.session.commit() - flash('Action updated') - return redirect(url_for('admin.actions')) + flash("Action updated") + return redirect(url_for("admin.actions")) - action_url = url_for('admin.edit_action', action_id=action.id) - return render_template('forms/simple_form.j2', title=u"Editing {}".format(action.name), form=form, - action_url=action_url) + action_url = url_for("admin.edit_action", action_id=action.id) + return render_template( + "forms/simple_form.j2", + title="Editing {}".format(action.name), + form=form, + action_url=action_url, + ) -@admin.route('/action/delete/', methods=['GET']) +@admin.route("/action/delete/", methods=["GET"]) @auth_required @admin_required def delete_action(action_id): @@ -308,88 +366,99 @@ def delete_action(action_id): aname = action.name db.session.delete(action) - message = u'Action {} deleted'.format(aname) - alert_type = 'alert-success' + message = "Action {} deleted".format(aname) + alert_type = "alert-success" try: db.session.commit() except IntegrityError: - message = u'Action {} is in use in some rules, can not be deleted!'.format(aname) - alert_type = 'alert-danger' + message = "Action {} is in use in some rules, can not be deleted!".format(aname) + alert_type = "alert-danger" flash(message, alert_type) - return redirect(url_for('admin.actions')) + return redirect(url_for("admin.actions")) -@admin.route('/communities') +@admin.route("/communities") @auth_required @admin_required def communities(): communities = db.session.query(Community).all() - return render_template('pages/communities.j2', communities=communities) + return render_template("pages/communities.j2", communities=communities) -@admin.route('/community', methods=['GET', 'POST']) +@admin.route("/community", methods=["GET", "POST"]) @auth_required @admin_required def community(): form = CommunityForm(request.form) - if request.method == 'POST' and form.validate(): + if request.method == "POST" and form.validate(): # test if Coomunity name is unique exist = get_existing_community(form.name.data) if not exist: - community = Community(name=form.name.data, - comm=form.comm.data, - larcomm=form.larcomm.data, - extcomm=form.extcomm.data, - description=form.description.data, - as_path=form.as_path.data, - role_id=form.role_id.data) + community = Community( + name=form.name.data, + comm=form.comm.data, + larcomm=form.larcomm.data, + extcomm=form.extcomm.data, + description=form.description.data, + as_path=form.as_path.data, + role_id=form.role_id.data, + ) db.session.add(community) db.session.commit() - flash('Community saved', 'alert-success') - return redirect(url_for('admin.communities')) + flash("Community saved", "alert-success") + return redirect(url_for("admin.communities")) else: - flash(u'Community with name {} already exists'.format( - form.name.data, form.command.data), 'alert-danger') + flash(f"Community with name {form.name.data} already exists", "alert-danger") - community_url = url_for('admin.community') - return render_template('forms/simple_form.j2', title="Add new community to Flowspec", form=form, - community_url=community_url) + community_url = url_for("admin.community") + return render_template( + "forms/simple_form.j2", + title="Add new community to Flowspec", + form=form, + community_url=community_url, + ) -@admin.route('/community/edit/', methods=['GET', 'POST']) +@admin.route("/community/edit/", methods=["GET", "POST"]) @auth_required @admin_required def edit_community(community_id): community = db.session.query(Community).get(community_id) print(community.role_id) form = CommunityForm(request.form, obj=community) - if request.method == 'POST' and form.validate(): + if request.method == "POST" and form.validate(): form.populate_obj(community) db.session.commit() - flash('Community updated') - return redirect(url_for('admin.communities')) + flash("Community updated") + return redirect(url_for("admin.communities")) - community_url = url_for('admin.edit_community', community_id=community.id) - return render_template('forms/simple_form.j2', title=u"Editing {}".format(community.name), form=form, - community_url=community_url) + community_url = url_for("admin.edit_community", community_id=community.id) + return render_template( + "forms/simple_form.j2", + title="Editing {}".format(community.name), + form=form, + community_url=community_url, + ) -@admin.route('/community/delete/', methods=['GET']) +@admin.route("/community/delete/", methods=["GET"]) @auth_required @admin_required def delete_community(community_id): community = db.session.query(Community).get(community_id) aname = community.name db.session.delete(community) - message = u'Community {} deleted'.format(aname) - alert_type = 'alert-success' + message = "Community {} deleted".format(aname) + alert_type = "alert-success" try: db.session.commit() except IntegrityError: - message = u'Community {} is in use in some rules, can not be deleted!'.format(aname) - alert_type = 'alert-danger' + message = "Community {} is in use in some rules, can not be deleted!".format( + aname + ) + alert_type = "alert-danger" flash(message, alert_type) - return redirect(url_for('admin.communities')) + return redirect(url_for("admin.communities")) diff --git a/run.py b/run.py index 106f0d2e..d497f4d0 100644 --- a/run.py +++ b/run.py @@ -8,10 +8,7 @@ app = create_app() # Configurations -try: - env = environ['EXAFS_ENV'] -except KeyError as e: - env = 'Production' +env = environ.get('EXAFS_ENV', 'Production') if env == 'devel': app.config.from_object(config.DevelopmentConfig) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..bf750d2c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 99 \ No newline at end of file From 79123f14a777dfb2eaf5b33f0019af0523d7e151 Mon Sep 17 00:00:00 2001 From: Jiri Vrany Date: Mon, 13 Mar 2023 19:30:55 +0100 Subject: [PATCH 09/43] dynamic menu from app.config --- config.example.py | 40 ++++--- flowapp/__init__.py | 31 +++++- flowapp/constants.py | 156 +++++++++++---------------- flowapp/templates/layouts/default.j2 | 24 ++--- 4 files changed, 117 insertions(+), 134 deletions(-) diff --git a/config.example.py b/config.example.py index 49981cd4..9cbc73f7 100644 --- a/config.example.py +++ b/config.example.py @@ -2,6 +2,7 @@ class Config(object): """ Default config options """ + # Flask debugging DEBUG = True # Flask testing @@ -9,55 +10,52 @@ class Config(object): # SSO auth enabled SSO_AUTH = False # SSO LOGOUT - LOGOUT_URL = 'https://flowspec.example.com/Shibboleth.sso/Logout?return=https://shibbo.example.com/idp/profile/Logout' + LOGOUT_URL = "https://flowspec.example.com/Shibboleth.sso/Logout" # SQL Alchemy config SQLALCHEMY_TRACK_MODIFICATIONS = False # URL of the ExaAPI - EXA_API_URL = 'http://localhost:5000/' + EXA_API_URL = "http://localhost:5000/" # Secret keys for Flask Session and JWT (API and CSRF protection) - JWT_SECRET = 'GenerateSomeLongRandomSequence' - SECRET_KEY = 'GenerateSomeLongRandomSequence' + JWT_SECRET = "GenerateSomeLongRandomSequence" + SECRET_KEY = "GenerateSomeLongRandomSequence" # LOCAL user parameters - when the app is used without SSO_AUTH # Defined in User model - LOCAL_USER_UUID = 'admin@example.com' + LOCAL_USER_UUID = "admin@example.com" # Defined in User model LOCAL_USER_ID = 1 # Defined in Role model / default 1 - view, 2 - normal user, 3 - admin - LOCAL_USER_ROLES = ['admin'] + LOCAL_USER_ROLES = ["admin"] # Defined in Organization model # List of organizations for the local user. There can be many of them. # Define the name and the adress range. The range is then used for first data insert # after the tables are created with db-init.py script. LOCAL_USER_ORGS = [ - { - 'name': 'Example Org.', - 'arange': '192.168.0.0/16\n2121:414:1a0b::/48' - }, + {"name": "Example Org.", "arange": "192.168.0.0/16\n2121:414:1a0b::/48"}, ] # Defined in Role model / default 1 - view, 2 - normal user, 3 - admin LOCAL_USER_ROLE_IDS = [3] # Defined in Organization model LOCAL_USER_ORG_IDS = [1] # APP Name - display in main toolbar - APP_NAME = 'ExaFS' + APP_NAME = "ExaFS" # Route Distinguisher for VRF - # When True set your rd string and label to be used in messages - USE_RD = True - RD_STRING = '7654:3210' - RD_LABEL = 'label for RD' - + # When True set your rd string and label to be used in messages + USE_RD = True + RD_STRING = "7654:3210" + RD_LABEL = "label for RD" class ProductionConfig(Config): """ Production config options """ + # SQL Alchemy config string - mustl include user and pwd - SQLALCHEMY_DATABASE_URI = 'mysql://user:password@localhost/exafs?charset=utf8' + SQLALCHEMY_DATABASE_URI = "Your Productionl Database URI" # Public IP of the production machine - LOCAL_IP = '127.0.0.1' + LOCAL_IP = "127.0.0.1" # SSO AUTH enabled in produciion SSO_AUTH = True # Set true if you need debug in production @@ -68,11 +66,11 @@ class DevelopmentConfig(Config): """ Development config options - usually for localhost development and debugging process """ - SQLALCHEMY_DATABASE_URI = 'mysql://root:my-secret-pw@127.0.0.1:3306/exafs?host=127.0.0.1?port=3306?charset=utf8' - LOCAL_IP = '127.0.0.1' + + SQLALCHEMY_DATABASE_URI = "Your Local Database URI" + LOCAL_IP = "127.0.0.1" DEBUG = True class TestingConfig(Config): - TESTING = True diff --git a/flowapp/__init__.py b/flowapp/__init__.py index 081326ec..1047708c 100644 --- a/flowapp/__init__.py +++ b/flowapp/__init__.py @@ -31,7 +31,7 @@ def create_app(): app.config.setdefault("VERSION", __version__) app.config.setdefault("SSO_ATTRIBUTE_MAP", SSO_ATTRIBUTE_MAP) app.config.setdefault("SSO_LOGIN_URL", "/login") - + # This attaches the *flask_sso* login handler to the SSO_LOGIN_URL, ext = SSO(app=app) @@ -58,6 +58,28 @@ def create_app(): app.register_blueprint(api_v3, url_prefix="/api/v3") app.register_blueprint(dashboard, url_prefix="/dashboard") + # menu items for the main menu + with app.test_request_context(): + app.config["MAIN_MENU"] = { + "edit": [ + {"name": "Add IPv4", "url": url_for('rules.ipv4_rule')}, + {"name": "Add IPv6", "url": url_for('rules.ipv6_rule')}, + {"name": "Add RTBH", "url": url_for('rules.rtbh_rule')}, + {"name": "API Key", "url": url_for('api_keys.all')}, + ], + "admin": [ + {"name": "Commands Log", "url": url_for('admin.log'), "divide_after": True}, + {"name": "Users", "url": url_for('admin.users')}, + {"name": "Add User", "url": url_for('admin.user')}, + {"name": "Organizations", "url": url_for('admin.organizations')}, + {"name": "Add Org.", "url": url_for('admin.organization'), "divide_after": True}, + {"name": "Action", "url": url_for('admin.actions')}, + {"name": "Add action", "url": url_for('admin.action')}, + {"name": "RTBH Communities", "url": url_for('admin.communities')}, + {"name": "Add RTBH Comm.", "url": url_for('admin.community')}, + ], + } + @ext.login_handler def login(user_info): try: @@ -150,11 +172,16 @@ def editable_rule(rule): return False return dict(editable_rule=editable_rule) + + @app.context_processor + def inject_main_menu(): + return {'main_menu': app.config.get('MAIN_MENU') } + @app.template_filter("strftime") def format_datetime(value): format = "y/MM/dd HH:mm" return babel.dates.format_datetime(value, format) - + return app diff --git a/flowapp/constants.py b/flowapp/constants.py index f232e847..077049f7 100644 --- a/flowapp/constants.py +++ b/flowapp/constants.py @@ -5,61 +5,57 @@ RULES_COLUMNS_V4 = ( - ('source', 'Source addr.'), - ('source_port', 'S port'), - ('dest', 'Dest. addr.'), - ('dest_port', 'D port'), - ('protocol', 'Proto'), - ('packet_len', 'Packet len'), - ('expires', 'Expires'), - ('action_id', 'Action'), - ('flags', 'Flags'), - ('user_id', 'User') + ("source", "Source addr."), + ("source_port", "S port"), + ("dest", "Dest. addr."), + ("dest_port", "D port"), + ("protocol", "Proto"), + ("packet_len", "Packet len"), + ("expires", "Expires"), + ("action_id", "Action"), + ("flags", "Flags"), + ("user_id", "User"), ) RULES_COLUMNS_V6 = ( - ('source', 'Source addr.'), - ('source_port', 'S port'), - ('dest', 'Dest. addr.'), - ('dest_port', 'D port'), - ('next_header', 'Next header'), - ('packet_len', 'Packet len'), - ('expires', 'Expires'), - ('action_id', 'Action'), - ('flags', 'Flags'), - ('user_id', 'User') + ("source", "Source addr."), + ("source_port", "S port"), + ("dest", "Dest. addr."), + ("dest_port", "D port"), + ("next_header", "Next header"), + ("packet_len", "Packet len"), + ("expires", "Expires"), + ("action_id", "Action"), + ("flags", "Flags"), + ("user_id", "User"), ) -DEFAULT_SORT = 'expires' -DEFAULT_ORDER = 'desc' +DEFAULT_SORT = "expires" +DEFAULT_ORDER = "desc" # Maximum allowed comma separated values for port string or packet lenght -MAX_COMMA_VALUES = 6 +MAX_COMMA_VALUES = 6 SORT_ARG = "sort" ORDER_ARG = "order" -RULE_ARG = 'rule_state' -TYPE_ARG = 'rule_type' -SEARCH_ARG = 'squery' -ORDSRC_ARG = 'ordsrc' -TIME_FORMAT_ARG = 'time_format' -TIME_YEAR="yearfirst" -TIME_US="us" -TIME_STMP="timestamp" - -RULES_KEY = 'rules' - -RULE_TYPES = { - 'ipv4': 4, - 'ipv6': 6, - 'rtbh': 1 -} +RULE_ARG = "rule_state" +TYPE_ARG = "rule_type" +SEARCH_ARG = "squery" +ORDSRC_ARG = "ordsrc" +TIME_FORMAT_ARG = "time_format" +TIME_YEAR = "yearfirst" +TIME_US = "us" +TIME_STMP = "timestamp" + +RULES_KEY = "rules" + +RULE_TYPES = {"ipv4": 4, "ipv6": 6, "rtbh": 1} RTBH_COLUMNS = ( - ('ipv4', 'IP adress (v4 or v6)'), - ('community_id', 'Community'), - ('expires', 'Expires'), - ('user_id', 'User') + ("ipv4", "IP adress (v4 or v6)"), + ("community_id", "Community"), + ("expires", "Expires"), + ("user_id", "User"), ) ANNOUNCE = 1 WITHDRAW = 2 @@ -68,69 +64,37 @@ MAX_PORT = 65535 MAX_PACKET = 9216 -IPV6_NEXT_HEADER = { - 'tcp': 'tcp', - 'udp': 'udp', - 'icmp': '58', - 'all': '' -} +IPV6_NEXT_HEADER = {"tcp": "tcp", "udp": "udp", "icmp": "58", "all": ""} -IPV4_PROTOCOL = { - 'tcp': 'tcp', - 'udp': 'udp', - 'icmp': 'icmp', - 'all': '' -} +IPV4_PROTOCOL = {"tcp": "tcp", "udp": "udp", "icmp": "icmp", "all": ""} IPV4_FRAGMENT = { - 'dont' : 'dont-fragment', - 'first' : 'first-fragment', - 'is' : 'is-fragment', - 'last' : 'last-fragment' + "dont": "dont-fragment", + "first": "first-fragment", + "is": "is-fragment", + "last": "last-fragment", } - RULE_TYPE_DISPATCH = { - 'ipv4': { - 'title': 'IPv4 rules', - 'columns': RULES_COLUMNS_V4 - }, - 'ipv6': { - 'title': 'IPv6 rules', - 'columns': RULES_COLUMNS_V6 - }, - 'rtbh': { - 'title': 'RTBH rules', - 'columns': RTBH_COLUMNS - } + "ipv4": {"title": "IPv4 rules", "columns": RULES_COLUMNS_V4}, + "ipv6": {"title": "IPv6 rules", "columns": RULES_COLUMNS_V6}, + "rtbh": {"title": "RTBH rules", "columns": RTBH_COLUMNS}, } -COLSPANS = { - 'rtbh': 5, - 'ipv4': 10, - 'ipv6': 10 -} +COLSPANS = {"rtbh": 5, "ipv4": 10, "ipv6": 10} -COMP_FUNCS = { - 'active': ge, - 'expired': lt, - 'all': None -} +COMP_FUNCS = {"active": ge, "expired": lt, "all": None} -COUNT_MATCH = { - 'ipv4':0, - 'ipv6':0, - 'rtbh':0 -} +COUNT_MATCH = {"ipv4": 0, "ipv6": 0, "rtbh": 0} TCP_FLAGS = [ - ('SYN', 'SYN'), - ('ACK', 'ACK'), - ('FIN', 'FIN'), - ('RST', 'RST'), - ('PUSH', 'PSH'), - ('URGENT', 'URG') - ] - -FORM_TIME_PATTERN = '%Y-%m-%dT%H:%M' \ No newline at end of file + ("SYN", "SYN"), + ("ACK", "ACK"), + ("FIN", "FIN"), + ("RST", "RST"), + ("PUSH", "PSH"), + ("URGENT", "URG"), +] + +FORM_TIME_PATTERN = "%Y-%m-%dT%H:%M" diff --git a/flowapp/templates/layouts/default.j2 b/flowapp/templates/layouts/default.j2 index b49a98b8..a8f903ad 100644 --- a/flowapp/templates/layouts/default.j2 +++ b/flowapp/templates/layouts/default.j2 @@ -36,10 +36,9 @@ - - {% endblock %} \ No newline at end of file diff --git a/flowapp/templates/forms/rule.j2 b/flowapp/templates/forms/rule.j2 index 88a0db14..5d03bc4d 100644 --- a/flowapp/templates/forms/rule.j2 +++ b/flowapp/templates/forms/rule.j2 @@ -102,12 +102,4 @@ - {% endblock %} \ No newline at end of file diff --git a/flowapp/validators.py b/flowapp/validators.py index 975057ed..f4569f86 100644 --- a/flowapp/validators.py +++ b/flowapp/validators.py @@ -302,13 +302,10 @@ def __init__(self, message=None): def __call__(self, form, field): try: - address = ipaddress.ip_address(field.data) + ipaddress.IPv6Address(field.data) except ValueError: raise ValidationError(self.message + str(field.data)) - if not isinstance(address, ipaddress.IPv6Address): - raise ValidationError(self.message + str(field.data)) - class IPv4Address(object): """ @@ -322,14 +319,10 @@ def __init__(self, message=None): def __call__(self, form, field): try: - address = ipaddress.ip_address(field.data) + ipaddress.IPv4Address(field.data) except ValueError: raise ValidationError(self.message + str(field.data)) - if not isinstance(address, ipaddress.IPv4Address): - raise ValidationError(self.message + str(field.data)) - - def editable_range(rule, net_ranges): """ check if the rule is editable for user From 9af2ec953f3d02ef8c6bd9202790b7449b1deec2 Mon Sep 17 00:00:00 2001 From: Jiri Vrany Date: Wed, 12 Jul 2023 13:41:02 +0200 Subject: [PATCH 43/43] bugfix messages multineighbor --- flowapp/messages.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/flowapp/messages.py b/flowapp/messages.py index 6870f8e6..9609eaf4 100644 --- a/flowapp/messages.py +++ b/flowapp/messages.py @@ -100,12 +100,11 @@ def create_rtbh(rule, message_type=ANNOUNCE): try: if current_app.config["USE_MULTI_NEIGHBOR"] and rule.community.comm: if rule.community.comm in current_app.config["MULTI_NEIGHBOR"].keys(): - target = current_app.config["MULTI_NEIGHBOR"].get(rule.community.comm) - neighbor = "neighbor {target} ".format(target=target) + targets = current_app.config["MULTI_NEIGHBOR"].get(rule.community.comm) else: targets = current_app.config["MULTI_NEIGHBOR"].get("primary") - neighbor = prepare_multi_neighbor(targets) - + + neighbor = prepare_multi_neighbor(targets) else: neighbor = "" except KeyError: @@ -253,9 +252,9 @@ def sanitize_mask(rule_mask, default_mask=IPV4_DEFMASK): return default_mask -def prepare_multi_neighbor(targets): +def prepare_multi_neighbor(targets: list): """ prepare multi neighbor string """ - neigbors = ["neighbor {}".format(tgt) for tgt in targets] + neigbors = [f"neighbor {tgt}" for tgt in targets] return ", ".join(neigbors) + " "