Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions config.example.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ class Config:
# list of RTBH Communities that are allowed to be used in whitelist, real ID from DB
ALLOWED_COMMUNITIES = [1, 2, 3]

# treshold for expired rule retation in days
EXPIRATION_THRESHOLD = 30


class ProductionConfig(Config):
"""
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.1.5"
__version__ = "1.1.6"
87 changes: 85 additions & 2 deletions flowapp/auth.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from functools import wraps
from typing import List, Optional
from flask import current_app, redirect, request, url_for, session, abort

from flowapp import __version__
from flowapp import __version__, db, validators
from flowapp.models import Flowspec4, Flowspec6, RTBH, Whitelist, get_user_nets


# auth atd.
def auth_required(f):
"""
auth required decorator
Expand Down Expand Up @@ -130,3 +131,85 @@ def get_user():
get user from session or return None
"""
return session.get("user_uuid", None)


def get_user_allowed_rule_ids(rule_type: str, user_id: int, user_role_ids: List[int]) -> List[int]:
"""
Get list of rule IDs that the user is allowed to modify.

For admin users (role_id 3), returns all rule IDs of the given type.
For regular users, returns only rules within their network ranges.

Args:
rule_type: Type of rule ('ipv4', 'ipv6', 'rtbh', 'whitelist')
user_id: Current user's ID
user_role_ids: List of user's role IDs

Returns:
List of rule IDs the user can modify
"""
# Admin users can modify any rules
if 3 in user_role_ids:
if rule_type == "ipv4":
return [r.id for r in db.session.query(Flowspec4.id).all()]
elif rule_type == "ipv6":
return [r.id for r in db.session.query(Flowspec6.id).all()]
elif rule_type == "rtbh":
return [r.id for r in db.session.query(RTBH.id).all()]
elif rule_type == "whitelist":
return [r.id for r in db.session.query(Whitelist.id).all()]
return []

# Regular users - filter by network ranges
net_ranges = get_user_nets(user_id)

if rule_type == "ipv4":
rules = db.session.query(Flowspec4).all()
filtered_rules = validators.filter_rules_in_network(net_ranges, rules)
return [r.id for r in filtered_rules]

elif rule_type == "ipv6":
rules = db.session.query(Flowspec6).all()
filtered_rules = validators.filter_rules_in_network(net_ranges, rules)
return [r.id for r in filtered_rules]

elif rule_type == "rtbh":
rules = db.session.query(RTBH).all()
filtered_rules = validators.filter_rtbh_rules(net_ranges, rules)
return [r.id for r in filtered_rules]

elif rule_type == "whitelist":
rules = db.session.query(Whitelist).all()
filtered_rules = validators.filter_rules_in_network(net_ranges, rules)
return [r.id for r in filtered_rules]

return []


def check_user_can_modify_rule(
rule_id: int, rule_type: str, user_id: Optional[int] = None, user_role_ids: Optional[List[int]] = None
) -> bool:
"""
Check if the current user can modify a specific rule.

Args:
rule_id: ID of the rule to check
rule_type: Type of rule ('ipv4', 'ipv6', 'rtbh', 'whitelist')
user_id: User ID (defaults to session user_id)
user_role_ids: User role IDs (defaults to session user_role_ids)

Returns:
True if user can modify the rule, False otherwise
"""
if user_id is None:
user_id = session.get("user_id")
if user_role_ids is None:
user_role_ids = session.get("user_role_ids", [])

# Admin users can modify any rules
if 3 in user_role_ids:
return True

# Check if rule_id is in allowed rules for this user
allowed_ids = get_user_allowed_rule_ids(rule_type, user_id, user_role_ids)
return rule_id in allowed_ids
77 changes: 77 additions & 0 deletions flowapp/services/rule_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,80 @@ def delete_rtbh_and_create_whitelist(
current_app.logger.exception(f"Error creating whitelist entry: {e}")
messages.append(f"Rule deleted but failed to create whitelist: {str(e)}")
return False, messages, None


def delete_expired_rules() -> Dict[str, int]:
"""
Delete all expired rules older than EXPIRATION_THRESHOLD days.
Only deletes rules in withdrawn or deleted state.

Returns:
Dictionary with deletion counts per rule type
"""
current_time = datetime.now()
expiration_threshold = current_app.config.get("EXPIRATION_THRESHOLD", 30)
deletion_date = current_time - timedelta(days=expiration_threshold)

deletion_counts = {"rtbh": 0, "ipv4": 0, "ipv6": 0, "total": 0}

model_map = {
"rtbh": (RTBH, RuleTypes.RTBH),
"ipv4": (Flowspec4, RuleTypes.IPv4),
"ipv6": (Flowspec6, RuleTypes.IPv6),
}

for rule_type, (model_class, rule_enum) in model_map.items():
# Get IDs of rules to delete
expired_rule_ids = [
r.id
for r in db.session.query(model_class.id)
.filter(
model_class.expires < deletion_date, model_class.rstate_id.in_([2, 3]) # withdrawn or deleted state
)
.all()
]

if not expired_rule_ids:
current_app.logger.info(f"No expired {model_class.__name__} rules to delete")
continue

# Clean up whitelist cache first
cache_deleted = 0
for rule_id in expired_rule_ids:
cache_deleted += RuleWhitelistCache.delete_by_rule_id(rule_id)

if cache_deleted:
current_app.logger.info(
f"Deleted {cache_deleted} cache entries for {len(expired_rule_ids)} {model_class.__name__} rules"
)

# Bulk delete the rules
deleted = (
db.session.query(model_class).filter(model_class.id.in_(expired_rule_ids)).delete(synchronize_session=False)
)

deletion_counts[rule_type] = deleted
deletion_counts["total"] += deleted

current_app.logger.info(
f"Deleted {deleted} expired {model_class.__name__} rules " f"(older than {expiration_threshold} days)"
)

# Commit all deletions at once
if deletion_counts["total"] > 0:
try:
db.session.commit()
current_app.logger.info(
f"Successfully deleted {deletion_counts['total']} expired rules: "
f"RTBH={deletion_counts['rtbh']}, "
f"IPv4={deletion_counts['ipv4']}, "
f"IPv6={deletion_counts['ipv6']}"
)
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error committing rule deletions: {e}")
raise
else:
current_app.logger.info("No expired rules found to delete")

return deletion_counts
2 changes: 0 additions & 2 deletions flowapp/views/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from flowapp import models, validators, flowspec
from flowapp.auth import auth_required
from flowapp.constants import (
RULES_KEY,
SORT_ARG,
ORDER_ARG,
DEFAULT_ORDER,
Expand Down Expand Up @@ -110,7 +109,6 @@ def index(rtype=None, rstate="active"):
# Enrich rules with whitelist information
rules, whitelist_rule_ids = enrich_rules_with_whitelist_info(rules, rtype)

session[RULES_KEY] = [rule.id for rule in rules]
# search rules
if get_search_query:
count_match = current_app.config["COUNT_MATCH"]
Expand Down
85 changes: 65 additions & 20 deletions flowapp/views/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
get_state_by_time,
round_to_ten_minutes,
)
from flowapp.auth import get_user_allowed_rule_ids, check_user_can_modify_rule


rules = Blueprint("rules", __name__, template_folder="templates")

Expand Down Expand Up @@ -158,20 +160,39 @@ def delete_rule(rule_type, rule_id):
# Convert the integer rule_type to RuleTypes enum
enum_rule_type = RuleTypes(rule_type)

# Get the rule type string for access checking
rule_type_map = {RuleTypes.IPv4.value: "ipv4", RuleTypes.IPv6.value: "ipv6", RuleTypes.RTBH.value: "rtbh"}
rule_type_str = rule_type_map.get(rule_type)

# Check if user can modify this rule
if not check_user_can_modify_rule(rule_id, rule_type_str):
flash("You cannot delete this rule", "alert-warning")
return redirect(
url_for(
"dashboard.index",
rtype=session[constants.TYPE_ARG],
rstate=session[constants.RULE_ARG],
sort=session[constants.SORT_ARG],
squery=session[constants.SEARCH_ARG],
order=session[constants.ORDER_ARG],
)
)

# Get allowed rule IDs for the service call
allowed_rule_ids = get_user_allowed_rule_ids(rule_type_str, session["user_id"], session["user_role_ids"])

# Use the service to delete the rule
success, message = rule_service.delete_rule(
rule_type=enum_rule_type,
rule_id=rule_id,
user_id=session["user_id"],
user_email=session["user_email"],
org_name=session["user_org"],
allowed_rule_ids=session.get(constants.RULES_KEY, []),
allowed_rule_ids=allowed_rule_ids,
)

# Flash appropriate message based on result
flash(message, "alert-success" if success else "alert-warning")

# Redirect back to dashboard
return redirect(
url_for(
"dashboard.index",
Expand All @@ -190,13 +211,19 @@ def delete_rule(rule_type, rule_id):
def delete_and_whitelist(rule_type, rule_id):
"""
Delete an RTBH rule and create a whitelist entry from it.

:param rule_id: integer - id of the RTBH rule
"""
if rule_type != RuleTypes.RTBH.value:
flash("Only RTBH rules can be converted to whitelists", "alert-warning")
return redirect(url_for("index"))

# Check if user can modify this rule
if not check_user_can_modify_rule(rule_id, "rtbh"):
flash("You cannot delete this rule", "alert-warning")
return redirect(url_for("index"))

# Get allowed rule IDs
allowed_rule_ids = get_user_allowed_rule_ids("rtbh", session["user_id"], session["user_role_ids"])

# Set whitelist expiration to 7 days from now by default
whitelist_expires = datetime.now() + timedelta(days=7)

Expand All @@ -207,19 +234,16 @@ def delete_and_whitelist(rule_type, rule_id):
org_id=session["user_org_id"],
user_email=session["user_email"],
org_name=session["user_org"],
allowed_rule_ids=session.get(constants.RULES_KEY, []),
allowed_rule_ids=allowed_rule_ids,
whitelist_expires=whitelist_expires,
)

# Flash all messages
for message in messages:
flash(message, "alert-success" if success else "alert-warning")

# If successful, flash additional message about whitelist
if success and whitelist:
flash(f"Created whitelist entry ID {whitelist.id} from RTBH rule", "alert-info")

# Redirect back to dashboard
return redirect(
url_for(
"dashboard.index",
Expand Down Expand Up @@ -269,10 +293,15 @@ def group_delete():
rule_type_int = constants.RULE_TYPES_DICT[rule_type]
enum_rule_type = RuleTypes(rule_type_int)
route_model = ROUTE_MODELS[rule_type_int]
rules = [str(x) for x in session[constants.RULES_KEY]]

# Get allowed rules for this user
allowed_rule_ids = get_user_allowed_rule_ids(rule_type, session["user_id"], session["user_role_ids"])
allowed_rules_str = [str(x) for x in allowed_rule_ids]

to_delete = request.form.getlist("delete-id")

if set(to_delete).issubset(set(rules)) or is_admin(session["user_roles"]):
# Check if user has permission to delete these rules
if set(to_delete).issubset(set(allowed_rules_str)) or is_admin(session["user_roles"]):
for rule_id in to_delete:
# withdraw route
model = db.session.get(model_name, rule_id)
Expand Down Expand Up @@ -321,11 +350,14 @@ def group_update():
rule_type = session[constants.TYPE_ARG]
form_name = DATA_FORMS_NAMED[rule_type]
to_update = request.form.getlist("delete-id")
rule_type = session[constants.TYPE_ARG]
rule_type_int = constants.RULE_TYPES_DICT[rule_type]
rules = [str(x) for x in session[constants.RULES_KEY]]

# Get allowed rules for this user
allowed_rule_ids = get_user_allowed_rule_ids(rule_type, session["user_id"], session["user_role_ids"])
allowed_rules_str = [str(x) for x in allowed_rule_ids]

# redirect bad request
if not set(to_update).issubset(set(rules)) or is_admin(session["user_roles"]):
if not set(to_update).issubset(set(allowed_rules_str)) and not is_admin(session["user_roles"]):
flash("You can't edit these rules!", "alert-danger")
return redirect(
url_for(
Expand Down Expand Up @@ -682,12 +714,25 @@ def announce_all():
@localhost_only
def withdraw_expired():
"""
cleaning endpoint
deletes expired whitelists
withdraws all expired routes from ExaBGP
deletes logs older than 30 days
Cleaning endpoint:
- Deletes expired whitelists
- Withdraws all expired routes from ExaBGP
- Deletes old expired rules
- Deletes logs older than 30 days
"""
delete_expired_whitelists()
# Delete expired whitelists
whitelist_messages = delete_expired_whitelists()
for msg in whitelist_messages:
current_app.logger.info(msg)

# Withdraw expired routes
announce_all_routes(constants.WITHDRAW)

# Delete old expired rules (in batches if needed)
deletion_counts = rule_service.delete_expired_rules()
current_app.logger.info(f"Deleted rules: {deletion_counts}")

# Delete old logs
Log.delete_old()
return " "

return "Cleanup completed"
5 changes: 4 additions & 1 deletion flowapp/views/whitelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from flowapp.models import get_user_nets, Whitelist
from flowapp.services import create_or_update_whitelist, delete_whitelist
from flowapp.utils.base import flash_errors
from flowapp.auth import check_user_can_modify_rule


whitelist = Blueprint("whitelist", __name__, template_folder="templates")

Expand Down Expand Up @@ -106,7 +108,8 @@ def delete(wl_id):
Delete whitelist
:param wl_id: integer - id of the whitelist
"""
if wl_id in session[constants.RULES_KEY]:
# Check if user can modify this whitelist
if check_user_can_modify_rule(wl_id, "whitelist"):
messages = delete_whitelist(wl_id)
flash(f"Whitelist {wl_id} deleted", "alert-success")
for message in messages:
Expand Down