Skip to content

Commit 17fab31

Browse files
authored
Merge pull request #58 from CESNET/develop
Merge develop 1.1.1 into Main
2 parents 866f3ba + b69e5bb commit 17fab31

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+8167
-2478
lines changed

.github/workflows/python-app.yml

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
1-
# This workflow will install Python dependencies, run tests and lint with a single version of Python
1+
# This workflow will install Python dependencies, run tests and lint with multiple versions of Python
22
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3-
43
name: Python application
5-
64
on:
75
push:
86
branches: [ "master", "develop" ]
97
pull_request:
108
branches: [ "master", "develop" ]
11-
129
permissions:
1310
contents: read
14-
1511
jobs:
1612
build:
17-
1813
runs-on: ubuntu-latest
19-
14+
strategy:
15+
matrix:
16+
python-version: ["3.9", "3.10", "3.11", "3.12"]
2017
steps:
2118
- uses: actions/checkout@v3
22-
- name: Set up Python 3.9
19+
- name: Set up Python ${{ matrix.python-version }}
2320
uses: actions/setup-python@v3
2421
with:
25-
python-version: "3.9"
22+
python-version: ${{ matrix.python-version }}
2623
- name: Setup timezone
2724
uses: zcong1993/setup-timezone@master
2825
with:

README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,39 @@ See how is ExaFS integrated into the network in the picture below.
3535
## System overview
3636

3737
![ExaFS schema](./docs/app_schema_en.png)
38+
The core component of ExaFS is a web application written in Python using the Flask framework. It provides a user interface for managing ExaBGP rules (CRUD operations) and also exposes a REST API with similar functionality. The web application uses Shibboleth for authentication, while the REST API relies on token-based authentication.
3839

39-
The central part of the ExaFS is a web application, written in Python3.6 with Flask framework. It provides a user interface for ExaBGP rule CRUD operations. The application also provides the REST API with CRUD operations for the configuration rules. The web app uses Shibboleth authorization; the REST API is using token-based authorization.
40+
The application generates ExaBGP commands and forwards them to the ExaBGP process. All rules are thoroughly validated—only valid rules are stored in the database and sent to the ExaBGP connector.
4041

41-
The app creates the ExaBGP commands and forwards them to ExaBGP process. All rules are carefully validated, and only valid rules are stored in the database and sent to the ExaBGP connector.
42-
43-
This second part of the system is another application that replicates the received command to the stdout. The connection between ExaBGP daemon and stdout of ExaAPI (ExaBGP process) is specified in the ExaBGP config.
42+
The second component of the system is a separate application that replicates received commands to `stdout`. The connection between the ExaBGP daemon and the `stdout` of the ExaAPI (ExaBGP process) is defined in the ExaBGP configuration.
4443

45-
This API was a part of the project, but now has been moved to own repository. You can use [pip package exabgp-process](https://pypi.org/project/exabgp-process/) or clone the git repo. Or you can create your own version.
46-
47-
Every time this process gets a command from ExaFS, it replicates this command to the ExaBGP service through the stdout. The registered service then updates the ExaBGP table – create, modify or remove the rule from command.
44+
This API was originally part of the same project but has since been moved to its own repository. You can use the [exabgp-process pip package](https://pypi.org/project/exabgp-process/), clone the Git repository, or develop your own implementation.
4845

49-
You may also need to monitor the ExaBGP and renew the commands after restart / shutdown. In docs you can find and example of system service named Guarda. This systemctl service is running in the host system and gets a notification on each restart of ExaBGP service via systemctl WantedBy config option. For every restart of ExaBGP the Guarda service will put all the valid and active rules to the ExaBGP rules table again.
46+
Each time this process receives a command from ExaFS, it outputs it to `stdout`, allowing the ExaBGP service to process the command and update its routing table—creating, modifying, or removing rules accordingly.
47+
48+
It may also be necessary to monitor ExaBGP and re-announce rules after a restart or shutdown. This can be handled via the ExaBGP service configuration, or by using an example system service called **Guarda**, described in the documentation. In either case, the key mechanism is calling the application endpoint `/rules/announce_all`. This endpoint is only accessible from `localhost`; a local IP address must be configured in the application settings.
5049

5150
## DOCS
51+
### Instalation related
52+
* [ExaFS Ansible deploy](https://github.com/CESNET/ExaFS-deploy) - repository with Ansbile playbook for deploying ExaFS with Docker Compose.
5253
* [Install notes](./docs/INSTALL.md)
53-
* [API documentation ](https://exafs.docs.apiary.io/#)
5454
* [Database backup configuration](./docs/DB_BACKUP.md)
5555
* [Local database instalation notes](./docs/DB_LOCAL.md)
56+
### API
57+
The REST API is documented using Swagger (OpenAPI). After installing and running the application, the API documentation is available locally at the /apidocs/ endpoint. This interactive documentation provides details about all available endpoints, request and response formats, and supported operations, making it easier to integrate and test the API.
58+
59+
5660

5761
## Change Log
62+
- 1.1.1 - Machine API Key rewrited.
63+
- API keys for machines are now tied to one of the existing users. If there is a need to have API access for machine, first create service user, and set the access rights. Then create machine key as Admin and assign it to this user.
64+
- 1.1.0 - Major Architecture Refactoring and Whitelist Integration
65+
- Code Organization and Architecture Improvements. Significant architectural refactoring focused on better separation of concerns and improved maintainability. The most notable change is the introduction of a dedicated **services layer** that extracts business logic from view controllers. Key service modules include `rule_service.py` for rule management operations, `whitelist_service.py` for whitelist functionality, and `whitelist_common.py` for shared whitelist utilities.
66+
- The **models structure** has been reorganized with better separation into logical modules. Rule models are now organized under `flowapp/models/rules/` with separate files for different rule types (`flowspec.py`, `rtbh.py`, `whitelist.py`), while maintaining backward compatibility through the main models `__init__.py`. Form handling has also been improved with better organization under `flowapp/forms/` and enhanced validation logic.
67+
- **RTBH Whitelist Integration** This system automatically evaluates new RTBH rules against existing whitelists and can automatically modify or block rules that conflict with whitelisted networks. When an RTBH rule is created that intersects with a whitelist entry, the system can:
68+
- **Automatically whitelist** rules that exactly match or are contained within whitelisted networks
69+
- **Create subnet rules** when RTBH rules are supersets of whitelisted networks, automatically generating the non-whitelisted portions
70+
- **Maintain rule cache** that tracks relationships between rules and whitelists for proper cleanup
5871
- 1.0.2 - fixed bug in IPv6 Flowspec messages
5972
- 1.0.1 . minor bug fixes
6073
- 1.0.0 . Major changes

docs/guarda-service/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Guarda Service for ExaBGP
2-
32
This is a systemd service designed to monitor ExaBGP and reapply commands after a restart or shutdown. The guarda.service runs on the host system and is triggered whenever the ExaBGP service restarts, thanks to the WantedBy configuration in systemd. After each restart, the Guarda service will reapply all valid and active rules to the ExaBGP rules table.
43

54
## Usage (as root)

flowapp/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.0.2"
1+
__version__ = "1.1.1"

flowapp/__init__.py

Lines changed: 25 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
# -*- coding: utf-8 -*-
2-
import babel
3-
import logging
4-
from loguru import logger
2+
from flask import Flask, redirect, render_template, session, url_for
53

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

2824

29-
class InterceptHandler(logging.Handler):
30-
31-
def emit(self, record):
32-
logger_opt = logger.opt(depth=6, exception=record.exc_info, colors=True)
33-
logger_opt.log(record.levelname, record.getMessage())
34-
35-
3625
def create_app(config_object=None):
3726
app = Flask(__name__)
3827

28+
# Load the default configuration for dashboard and main menu
29+
app.config.from_object(InstanceConfig)
30+
if config_object:
31+
app.config.from_object(config_object)
32+
33+
app.config.setdefault("VERSION", __version__)
34+
3935
# SSO configuration
4036
SSO_ATTRIBUTE_MAP = {
4137
"eppn": (True, "eppn"),
@@ -47,13 +43,6 @@ def create_app(config_object=None):
4743
migrate.init_app(app, db)
4844
csrf.init_app(app)
4945

50-
# Load the default configuration for dashboard and main menu
51-
app.config.from_object(InstanceConfig)
52-
if config_object:
53-
app.config.from_object(config_object)
54-
55-
app.config.setdefault("VERSION", __version__)
56-
5746
# Init SSO
5847
ext.init_app(app)
5948

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

67-
from flowapp import models, constants, validators
68-
from .views.admin import admin
69-
from .views.rules import rules
70-
from .views.api_v1 import api as api_v1
71-
from .views.api_v2 import api as api_v2
72-
from .views.api_v3 import api as api_v3
73-
from .views.api_keys import api_keys
56+
from flowapp import models, constants
7457
from .auth import auth_required
75-
from .views.dashboard import dashboard
76-
77-
# no need for csrf on api because we use JWT
78-
csrf.exempt(api_v1)
79-
csrf.exempt(api_v2)
80-
csrf.exempt(api_v3)
81-
82-
app.register_blueprint(admin, url_prefix="/admin")
83-
app.register_blueprint(rules, url_prefix="/rules")
84-
app.register_blueprint(api_keys, url_prefix="/api_keys")
85-
app.register_blueprint(api_v1, url_prefix="/api/v1")
86-
app.register_blueprint(api_v2, url_prefix="/api/v2")
87-
app.register_blueprint(api_v3, url_prefix="/api/v3")
88-
app.register_blueprint(dashboard, url_prefix="/dashboard")
89-
90-
# register loguru as handler
91-
app.logger.removeHandler(default_handler)
92-
app.logger.addHandler(InterceptHandler())
93-
94-
@ext.login_handler
95-
def login(user_info):
96-
try:
97-
uuid = user_info.get("eppn")
98-
except KeyError:
99-
uuid = False
100-
return render_template("errors/401.html")
10158

102-
return _handle_login(uuid)
59+
# Register blueprints
60+
from .utils import register_blueprints
10361

104-
@app.route("/logout")
105-
def logout():
106-
session["user_uuid"] = False
107-
session["user_id"] = False
108-
session.clear()
109-
return redirect(app.config.get("LOGOUT_URL"))
62+
register_blueprints(app, csrf)
11063

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

117-
uuid = request.headers.get(header_name)
118-
if not uuid:
119-
return render_template("errors/401.html")
67+
configure_logging(app)
12068

121-
return _handle_login(uuid)
69+
# register error handlers
70+
from .utils import register_error_handlers
12271

123-
@app.route("/local-login")
124-
def local_login():
125-
print("Local login started")
126-
if not app.config.get("LOCAL_AUTH", False):
127-
print("Local auth not enabled")
128-
return render_template("errors/401.html")
72+
register_error_handlers(app)
12973

130-
uuid = app.config.get("LOCAL_USER_UUID", False)
131-
if not uuid:
132-
print("Local user not set")
133-
return render_template("errors/401.html")
74+
# register auth handlers
75+
from .utils import register_auth_handlers
13476

135-
print(f"Local login with {uuid}")
136-
return _handle_login(uuid)
77+
register_auth_handlers(app, ext)
13778

13879
@app.route("/")
13980
@auth_required
@@ -192,89 +133,10 @@ def select_org(org_id=None):
192133
def shutdown_session(exception=None):
193134
db.session.remove()
194135

195-
# HTTP error handling
196-
@app.errorhandler(404)
197-
def not_found(error):
198-
return render_template("errors/404.html"), 404
199-
200-
@app.errorhandler(500)
201-
def internal_error(exception):
202-
app.logger.exception(exception)
203-
return render_template("errors/500.html"), 500
204-
205-
@app.context_processor
206-
def utility_processor():
207-
def editable_rule(rule):
208-
if rule:
209-
validators.editable_range(rule, models.get_user_nets(session["user_id"]))
210-
return True
211-
return False
212-
213-
return dict(editable_rule=editable_rule)
214-
215-
@app.context_processor
216-
def inject_main_menu():
217-
"""
218-
inject main menu config to templates
219-
used in default template to create main menu
220-
"""
221-
return {"main_menu": app.config.get("MAIN_MENU")}
222-
223-
@app.context_processor
224-
def inject_dashboard():
225-
"""
226-
inject dashboard config to templates
227-
used in submenu dashboard to create dashboard tables
228-
"""
229-
return {"dashboard": app.config.get("DASHBOARD")}
230-
231-
@app.template_filter("strftime")
232-
def format_datetime(value):
233-
if value is None:
234-
return app.config.get("MISSING_DATETIME_MESSAGE", "Never")
235-
236-
format = "y/MM/dd HH:mm"
237-
return babel.dates.format_datetime(value, format)
238-
239-
@app.template_filter("unlimited")
240-
def unlimited_filter(value):
241-
return "unlimited" if value == 0 else value
242-
243-
def _handle_login(uuid: str):
244-
"""
245-
handles rest of login process
246-
"""
247-
multiple_orgs = False
248-
try:
249-
user, multiple_orgs = _register_user_to_session(uuid)
250-
except AttributeError as e:
251-
app.logger.exception(e)
252-
return render_template("errors/401.html")
253-
254-
if multiple_orgs:
255-
return redirect(url_for("select_org", org_id=None))
136+
# register context processors and template filters
137+
from .utils import register_context_processors, register_template_filters
256138

257-
# set user org to session
258-
user_org = user.organization.first()
259-
session["user_org"] = user_org.name
260-
session["user_org_id"] = user_org.id
261-
262-
return redirect("/")
263-
264-
def _register_user_to_session(uuid: str):
265-
print(f"Registering user {uuid} to session")
266-
user = db.session.query(models.User).filter_by(uuid=uuid).first()
267-
print(f"Got user {user} from DB")
268-
session["user_uuid"] = user.uuid
269-
session["user_email"] = user.uuid
270-
session["user_name"] = user.name
271-
session["user_id"] = user.id
272-
session["user_roles"] = [role.name for role in user.role.all()]
273-
session["user_role_ids"] = [role.id for role in user.role.all()]
274-
roles = [i > 1 for i in session["user_role_ids"]]
275-
session["can_edit"] = True if all(roles) and roles else []
276-
# check if user has multiple organizations and return True if so
277-
print(f"DEBUG SESSION {session}")
278-
return user, len(user.organization.all()) > 1
139+
register_context_processors(app)
140+
register_template_filters(app)
279141

280142
return app

flowapp/constants.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
This module contains constant values used in application
33
"""
44

5+
from enum import Enum
56
from operator import ge, lt
67

78
DEFAULT_SORT = "expires"
@@ -59,7 +60,12 @@
5960
FORM_TIME_PATTERN = "%Y-%m-%dT%H:%M"
6061

6162

62-
class RuleTypes:
63+
class RuleTypes(Enum):
6364
RTBH = 1
6465
IPv4 = 4
6566
IPv6 = 6
67+
68+
69+
class RuleOrigin(Enum):
70+
USER = 1
71+
WHITELIST = 2

0 commit comments

Comments
 (0)