Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
94d03cc
first prototype of whitelist model and form
jirivrany Feb 20, 2025
073c2b4
fixed api after convetting RuleTypes to Enum class
jirivrany Feb 20, 2025
f6672dc
add more test for validators, fix edge cases validation in flowspec a…
jirivrany Feb 20, 2025
4e2a6a8
increased test coverage for forms, fixed minor edge cases validation …
jirivrany Feb 21, 2025
c3b2bbb
Refactor equality checks and add model unit tests
jirivrany Feb 21, 2025
d86634e
whitelist view stub - WIP
jirivrany Feb 27, 2025
af4befc
Refactor models into a separate directory and update references
jirivrany Feb 27, 2025
6fa4bb4
bugfix - RuleTypes use in api_common and output
jirivrany Feb 28, 2025
2425383
refactoring forms.py into directory forms for better readibility and …
jirivrany Feb 28, 2025
500d60a
refactoring of app factory in flowap/__init__.py module.
jirivrany Feb 28, 2025
9b37913
Refactor rule creation logic to use rule_service
jirivrany Mar 10, 2025
74bfefe
Done - add whitelist, display whitelist in dashboard. WIP - basic whi…
jirivrany Mar 11, 2025
5b22513
whitelist time update and delete completed, whitelist cache clean imp…
jirivrany Mar 12, 2025
356e1f3
refactoring of whitelist services / split into common lib for rules a…
jirivrany Mar 12, 2025
b765603
refactoring of services - broken tests for rule and whitelist services
jirivrany Mar 13, 2025
8b499bc
refactoring of services - fixed tests for rule and whitelist services…
jirivrany Mar 13, 2025
2fcdb65
Improve rule management, logging, and RabbitMQ connection handling
jirivrany Mar 14, 2025
04b5de1
Enhance rule visibility and styling in dashboard tables
jirivrany Mar 14, 2025
ed594cf
Improve rule-whitelist interactions and UI updates
jirivrany Mar 14, 2025
7ca0b03
fixed app config for tests
jirivrany Mar 14, 2025
72fd0c9
Enhance IPv4/IPv6 checks by introducing _is_same_ip_version, update r…
jirivrany Mar 14, 2025
c9969c6
improved logging of whitelist service
jirivrany Mar 17, 2025
412dd84
Refactor create_app config loading, enhance RTBH logging and flash me…
jirivrany Mar 17, 2025
eb4c683
integration test for RTBH api and whitelist, minor bug fix in utils
jirivrany Mar 18, 2025
354cb53
[200~Refactor route announcement and whitelist expiration handling
jirivrany Mar 18, 2025
910309d
# Add log cleanup functionality and enhance logging
jirivrany Mar 20, 2025
0b8a2bf
Refactor rule reactivation logic and improve limit handling
jirivrany Mar 21, 2025
bcdcc54
modified Rule service to check RTBH rule whitelisting during rule rea…
jirivrany Mar 21, 2025
edde801
bugfix - search for whitelist, added dict method to model
jirivrany Mar 24, 2025
968388c
updated dashboard / no group operations for whitelist
jirivrany Mar 25, 2025
1b99e1a
Add functionality to delete RTBH rules and create whitelist entries
jirivrany Mar 25, 2025
90ddbe7
tests for new methods of rule service
jirivrany Mar 25, 2025
a292a16
fix comment for whitelist created from deleted rtbh rule
jirivrany Mar 25, 2025
fbd7ade
delete to whitelist button is now displayed only for rules with allow…
jirivrany Mar 26, 2025
f224796
fix help for RTBH form, add tooltips to all table buttons
jirivrany Apr 6, 2025
6bc0c5a
fixed version number, minor template formating changes for RTBH
jirivrany Apr 7, 2025
f05bd4d
fixed machine api key, key now have user rights of user, not of admin…
jirivrany Jun 3, 2025
b836a1b
Update python-app.yml
jirivrany Jun 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
- name: Set up Python 3.11
uses: actions/setup-python@v3
with:
python-version: "3.9"
python-version: "3.11"
- name: Setup timezone
uses: zcong1993/setup-timezone@master
with:
Expand Down
2 changes: 1 addition & 1 deletion flowapp/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.2"
__version__ = "1.1.1"
188 changes: 25 additions & 163 deletions flowapp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
# -*- coding: utf-8 -*-
import babel
import logging
from loguru import logger
from flask import Flask, redirect, render_template, session, url_for

from flask import Flask, redirect, render_template, session, url_for, request
from flask.logging import default_handler
from flask_sso import SSO
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
Expand All @@ -26,16 +22,16 @@
swagger = Swagger(template_file="static/swagger.yml")


class InterceptHandler(logging.Handler):

def emit(self, record):
logger_opt = logger.opt(depth=6, exception=record.exc_info, colors=True)
logger_opt.log(record.levelname, record.getMessage())


def create_app(config_object=None):
app = Flask(__name__)

# Load the default configuration for dashboard and main menu
app.config.from_object(InstanceConfig)
if config_object:
app.config.from_object(config_object)

app.config.setdefault("VERSION", __version__)

# SSO configuration
SSO_ATTRIBUTE_MAP = {
"eppn": (True, "eppn"),
Expand All @@ -47,13 +43,6 @@ def create_app(config_object=None):
migrate.init_app(app, db)
csrf.init_app(app)

# Load the default configuration for dashboard and main menu
app.config.from_object(InstanceConfig)
if config_object:
app.config.from_object(config_object)

app.config.setdefault("VERSION", __version__)

# Init SSO
ext.init_app(app)

Expand All @@ -64,76 +53,28 @@ def create_app(config_object=None):
if app.config.get("BEHIND_PROXY", False):
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)

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 flowapp import models, constants
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")

# register loguru as handler
app.logger.removeHandler(default_handler)
app.logger.addHandler(InterceptHandler())

@ext.login_handler
def login(user_info):
try:
uuid = user_info.get("eppn")
except KeyError:
uuid = False
return render_template("errors/401.html")

return _handle_login(uuid)
# Register blueprints
from .utils import register_blueprints

@app.route("/logout")
def logout():
session["user_uuid"] = False
session["user_id"] = False
session.clear()
return redirect(app.config.get("LOGOUT_URL"))
register_blueprints(app, csrf)

@app.route("/ext-login")
def ext_login():
header_name = app.config.get("AUTH_HEADER_NAME", "X-Authenticated-User")
if header_name not in request.headers:
return render_template("errors/401.html")
# configure logging
from .utils import configure_logging

uuid = request.headers.get(header_name)
if not uuid:
return render_template("errors/401.html")
configure_logging(app)

return _handle_login(uuid)
# register error handlers
from .utils import register_error_handlers

@app.route("/local-login")
def local_login():
print("Local login started")
if not app.config.get("LOCAL_AUTH", False):
print("Local auth not enabled")
return render_template("errors/401.html")
register_error_handlers(app)

uuid = app.config.get("LOCAL_USER_UUID", False)
if not uuid:
print("Local user not set")
return render_template("errors/401.html")
# register auth handlers
from .utils import register_auth_handlers

print(f"Local login with {uuid}")
return _handle_login(uuid)
register_auth_handlers(app, ext)

@app.route("/")
@auth_required
Expand Down Expand Up @@ -192,89 +133,10 @@ def select_org(org_id=None):
def shutdown_session(exception=None):
db.session.remove()

# HTTP error handling
@app.errorhandler(404)
def not_found(error):
return render_template("errors/404.html"), 404

@app.errorhandler(500)
def internal_error(exception):
app.logger.exception(exception)
return render_template("errors/500.html"), 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

return dict(editable_rule=editable_rule)

@app.context_processor
def inject_main_menu():
"""
inject main menu config to templates
used in default template to create main menu
"""
return {"main_menu": app.config.get("MAIN_MENU")}

@app.context_processor
def inject_dashboard():
"""
inject dashboard config to templates
used in submenu dashboard to create dashboard tables
"""
return {"dashboard": app.config.get("DASHBOARD")}

@app.template_filter("strftime")
def format_datetime(value):
if value is None:
return app.config.get("MISSING_DATETIME_MESSAGE", "Never")

format = "y/MM/dd HH:mm"
return babel.dates.format_datetime(value, format)

@app.template_filter("unlimited")
def unlimited_filter(value):
return "unlimited" if value == 0 else value

def _handle_login(uuid: str):
"""
handles rest of login process
"""
multiple_orgs = False
try:
user, multiple_orgs = _register_user_to_session(uuid)
except AttributeError as e:
app.logger.exception(e)
return render_template("errors/401.html")

if multiple_orgs:
return redirect(url_for("select_org", org_id=None))
# register context processors and template filters
from .utils import register_context_processors, register_template_filters

# set user org to session
user_org = user.organization.first()
session["user_org"] = user_org.name
session["user_org_id"] = user_org.id

return redirect("/")

def _register_user_to_session(uuid: str):
print(f"Registering user {uuid} to session")
user = db.session.query(models.User).filter_by(uuid=uuid).first()
print(f"Got user {user} from DB")
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_role_ids"] = [role.id for role in user.role.all()]
roles = [i > 1 for i in session["user_role_ids"]]
session["can_edit"] = True if all(roles) and roles else []
# check if user has multiple organizations and return True if so
print(f"DEBUG SESSION {session}")
return user, len(user.organization.all()) > 1
register_context_processors(app)
register_template_filters(app)

return app
8 changes: 7 additions & 1 deletion flowapp/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This module contains constant values used in application
"""

from enum import Enum
from operator import ge, lt

DEFAULT_SORT = "expires"
Expand Down Expand Up @@ -59,7 +60,12 @@
FORM_TIME_PATTERN = "%Y-%m-%dT%H:%M"


class RuleTypes:
class RuleTypes(Enum):
RTBH = 1
IPv4 = 4
IPv6 = 6


class RuleOrigin(Enum):
USER = 1
WHITELIST = 2
46 changes: 27 additions & 19 deletions flowapp/flowspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ def translate_sequence(sequence, max_val=MAX_PORT):
return "[{}]".format(result)


def check_limit(value, max_value, min_value=0):
"""
test if the value is within valid range (min_value to max_value inclusive)
raise exception otherwise
"""
value = int(value)
if value > max_value:
raise ValueError("Invalid value number: {} is too big. Max is {}.".format(value, max_value))
if value < min_value:
raise ValueError("Invalid value number: {} is too small. Min is {}.".format(value, min_value))
return value


def to_exabgp_string(value_string, max_val):
"""
Translate form string to flowspec value or packet size rule
Expand All @@ -42,35 +55,30 @@ def to_exabgp_string(value_string, max_val):
return "={}".format(check_limit(value_string, max_val))
elif RANGE.match(value_string):
m = RANGE.match(value_string)
return ">={}&<={}".format(
check_limit(m.group(1), max_val), check_limit(m.group(2), max_val)
)
start = check_limit(m.group(1), max_val)
end = check_limit(m.group(2), max_val)
if start > end:
raise ValueError("Invalid range: start value cannot be greater than end value")
return ">={}&<={}".format(start, end)
elif NOTRAN.match(value_string):
return value_string
m = NOTRAN.match(value_string)
start = check_limit(m.group(1), max_val)
end = check_limit(m.group(2), max_val)
if start > end:
raise ValueError("Invalid range: start value cannot be greater than end value")
return ">={}&<={}".format(start, end)
elif GREATER.match(value_string):
m = GREATER.match(value_string)
return ">={}&<={}".format(check_limit(m.group(1), max_val), max_val)
elif LOWER.match(value_string):
m = LOWER.match(value_string)
return ">=0&<={}".format(check_limit(m.group(1), max_val))
# Even for lower bound expressions, validate that the value itself is within range
end = check_limit(m.group(1), max_val)
return ">=0&<={}".format(end)
else:
raise ValueError("string {} can not be converted".format(value_string))


def check_limit(value, max_value):
"""
test if the value is lower than max_value
raise exception otherwise
"""
value = int(value)
if value > max_value:
raise ValueError(
"Invalid value number: {} is too big. Max is {}.".format(value, max_value)
)
else:
return value


def filter_rules_action(user_actions, rules):
"""
Divide the list of rules by user_actions to editable and viewonly subsets
Expand Down
Loading