diff --git a/config.example.py b/config.example.py index 80dc9f2..d5994e2 100644 --- a/config.example.py +++ b/config.example.py @@ -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): """ diff --git a/flowapp/__about__.py b/flowapp/__about__.py index c599dc3..aaf9610 100755 --- a/flowapp/__about__.py +++ b/flowapp/__about__.py @@ -1 +1 @@ -__version__ = "1.1.5" +__version__ = "1.1.6" diff --git a/flowapp/auth.py b/flowapp/auth.py index 2f12cee..a26628b 100644 --- a/flowapp/auth.py +++ b/flowapp/auth.py @@ -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 @@ -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 diff --git a/flowapp/services/rule_service.py b/flowapp/services/rule_service.py index fca7719..7741528 100644 --- a/flowapp/services/rule_service.py +++ b/flowapp/services/rule_service.py @@ -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 diff --git a/flowapp/views/dashboard.py b/flowapp/views/dashboard.py index 4431927..b4cd176 100644 --- a/flowapp/views/dashboard.py +++ b/flowapp/views/dashboard.py @@ -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, @@ -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"] diff --git a/flowapp/views/rules.py b/flowapp/views/rules.py index 7a50dff..b871eec 100644 --- a/flowapp/views/rules.py +++ b/flowapp/views/rules.py @@ -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") @@ -158,6 +160,27 @@ 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, @@ -165,13 +188,11 @@ def delete_rule(rule_type, 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", @@ -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) @@ -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", @@ -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) @@ -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( @@ -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" diff --git a/flowapp/views/whitelist.py b/flowapp/views/whitelist.py index 9cc85e4..743174c 100644 --- a/flowapp/views/whitelist.py +++ b/flowapp/views/whitelist.py @@ -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") @@ -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: