feat: api rate limiting

This commit is contained in:
2025-12-27 01:26:43 +00:00
parent 181e2b2ca5
commit e316a16386
3 changed files with 60 additions and 3 deletions
+11 -1
View File
@@ -1,4 +1,6 @@
from flask import Flask, session from flask import Flask, session
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from db import init_db, hash_password, get_db_connection from db import init_db, hash_password, get_db_connection
from routes import register_routes from routes import register_routes
import os import os
@@ -15,6 +17,14 @@ app.config['MYSQL_USER'] = os.environ.get('MYSQL_USER', 'user')
app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD', 'password') app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD', 'password')
app.config['MYSQL_DATABASE'] = os.environ.get('MYSQL_DATABASE', 'ipam') app.config['MYSQL_DATABASE'] = os.environ.get('MYSQL_DATABASE', 'ipam')
# Initialize rate limiter
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per hour", "50 per minute"],
storage_uri="memory://"
)
@app.context_processor @app.context_processor
def inject_env_vars(): def inject_env_vars():
version = 'unknown' version = 'unknown'
@@ -36,7 +46,7 @@ def inject_env_vars():
'has_permission': has_permission 'has_permission': has_permission
} }
register_routes(app) register_routes(app, limiter)
init_db(app) init_db(app)
# Start cache pre-warming in background # Start cache pre-warming in background
+2 -1
View File
@@ -4,4 +4,5 @@ dotenv
gunicorn gunicorn
requests requests
pyotp pyotp
qrcode[pil] qrcode[pil]
Flask-Limiter
+47 -1
View File
@@ -404,8 +404,19 @@ def prewarm_cache(app):
thread.start() thread.start()
logging.info("Started background cache pre-warming thread") logging.info("Started background cache pre-warming thread")
def register_routes(app): def register_routes(app, limiter=None):
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# Helper function to apply rate limiting if limiter is available
def rate_limit(limit_str):
"""Apply rate limiting decorator if limiter is available"""
if limiter:
return limiter.limit(limit_str)
else:
# Return a no-op decorator if limiter is not available
def noop_decorator(f):
return f
return noop_decorator
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
@@ -2792,6 +2803,7 @@ def register_routes(app):
# ========== API ROUTES ========== # ========== API ROUTES ==========
@app.route('/api/v1/info', methods=['GET']) @app.route('/api/v1/info', methods=['GET'])
@rate_limit("100 per minute")
@api_auth_required @api_auth_required
def api_info(): def api_info():
"""Get API information and authenticated user info""" """Get API information and authenticated user info"""
@@ -2806,6 +2818,7 @@ def register_routes(app):
# Devices API # Devices API
@app.route('/api/v1/devices', methods=['GET']) @app.route('/api/v1/devices', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_devices') @api_permission_required('view_devices')
def api_devices(): def api_devices():
"""Get all devices""" """Get all devices"""
@@ -2839,6 +2852,7 @@ def register_routes(app):
return jsonify({'devices': devices}) return jsonify({'devices': devices})
@app.route('/api/v1/devices/<int:device_id>', methods=['GET']) @app.route('/api/v1/devices/<int:device_id>', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_device') @api_permission_required('view_device')
def api_device(device_id): def api_device(device_id):
"""Get a specific device""" """Get a specific device"""
@@ -2873,6 +2887,7 @@ def register_routes(app):
return jsonify(device) return jsonify(device)
@app.route('/api/v1/devices', methods=['POST']) @app.route('/api/v1/devices', methods=['POST'])
@rate_limit("50 per minute")
@api_permission_required('add_device') @api_permission_required('add_device')
def api_add_device(): def api_add_device():
"""Create a new device""" """Create a new device"""
@@ -2895,6 +2910,7 @@ def register_routes(app):
return jsonify({'id': device_id, 'name': name, 'description': description, 'device_type_id': device_type_id}), 201 return jsonify({'id': device_id, 'name': name, 'description': description, 'device_type_id': device_type_id}), 201
@app.route('/api/v1/devices/<int:device_id>', methods=['PUT']) @app.route('/api/v1/devices/<int:device_id>', methods=['PUT'])
@rate_limit("50 per minute")
@api_permission_required('edit_device') @api_permission_required('edit_device')
def api_update_device(device_id): def api_update_device(device_id):
"""Update a device""" """Update a device"""
@@ -2942,6 +2958,7 @@ def register_routes(app):
return jsonify({'message': 'Device updated successfully', 'device': {'id': device_id, 'name': new_name}}) return jsonify({'message': 'Device updated successfully', 'device': {'id': device_id, 'name': new_name}})
@app.route('/api/v1/devices/<int:device_id>', methods=['DELETE']) @app.route('/api/v1/devices/<int:device_id>', methods=['DELETE'])
@rate_limit("50 per minute")
@api_permission_required('delete_device') @api_permission_required('delete_device')
def api_delete_device(device_id): def api_delete_device(device_id):
"""Delete a device""" """Delete a device"""
@@ -2978,6 +2995,7 @@ def register_routes(app):
return jsonify({'message': 'Device deleted successfully', 'device': {'id': device_id, 'name': device_name}}) return jsonify({'message': 'Device deleted successfully', 'device': {'id': device_id, 'name': device_name}})
@app.route('/api/v1/devices/<int:device_id>/ips', methods=['POST']) @app.route('/api/v1/devices/<int:device_id>/ips', methods=['POST'])
@rate_limit("50 per minute")
@api_permission_required('add_device_ip') @api_permission_required('add_device_ip')
def api_add_device_ip(device_id): def api_add_device_ip(device_id):
"""Add an IP address to a device""" """Add an IP address to a device"""
@@ -3040,6 +3058,7 @@ def register_routes(app):
return jsonify({'message': 'IP address added to device successfully', 'ip_id': ip_id}), 201 return jsonify({'message': 'IP address added to device successfully', 'ip_id': ip_id}), 201
@app.route('/api/v1/devices/<int:device_id>/ips/<int:ip_id>', methods=['DELETE']) @app.route('/api/v1/devices/<int:device_id>/ips/<int:ip_id>', methods=['DELETE'])
@rate_limit("50 per minute")
@api_permission_required('remove_device_ip') @api_permission_required('remove_device_ip')
def api_remove_device_ip(device_id, ip_id): def api_remove_device_ip(device_id, ip_id):
"""Remove an IP address from a device""" """Remove an IP address from a device"""
@@ -3072,6 +3091,7 @@ def register_routes(app):
# Subnets API # Subnets API
@app.route('/api/v1/subnets', methods=['GET']) @app.route('/api/v1/subnets', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_subnet') @api_permission_required('view_subnet')
def api_subnets(): def api_subnets():
"""Get all subnets""" """Get all subnets"""
@@ -3089,6 +3109,7 @@ def register_routes(app):
return jsonify({'subnets': subnets}) return jsonify({'subnets': subnets})
@app.route('/api/v1/subnets/<int:subnet_id>', methods=['GET']) @app.route('/api/v1/subnets/<int:subnet_id>', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_subnet') @api_permission_required('view_subnet')
def api_subnet(subnet_id): def api_subnet(subnet_id):
"""Get a specific subnet with IP addresses""" """Get a specific subnet with IP addresses"""
@@ -3111,6 +3132,7 @@ def register_routes(app):
return jsonify(subnet) return jsonify(subnet)
@app.route('/api/v1/subnets/<int:subnet_id>/next_free_ip', methods=['GET']) @app.route('/api/v1/subnets/<int:subnet_id>/next_free_ip', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_subnet') @api_permission_required('view_subnet')
def api_subnet_next_free_ip(subnet_id): def api_subnet_next_free_ip(subnet_id):
"""Get the next free IP address in a subnet""" """Get the next free IP address in a subnet"""
@@ -3138,6 +3160,7 @@ def register_routes(app):
return jsonify({'id': result['id'], 'ip': result['ip']}) return jsonify({'id': result['id'], 'ip': result['ip']})
@app.route('/api/v1/subnets', methods=['POST']) @app.route('/api/v1/subnets', methods=['POST'])
@rate_limit("50 per minute")
@api_permission_required('add_subnet') @api_permission_required('add_subnet')
def api_add_subnet(): def api_add_subnet():
"""Create a new subnet""" """Create a new subnet"""
@@ -3168,6 +3191,7 @@ def register_routes(app):
return jsonify({'id': subnet_id, 'name': name, 'cidr': cidr, 'site': site}), 201 return jsonify({'id': subnet_id, 'name': name, 'cidr': cidr, 'site': site}), 201
@app.route('/api/v1/subnets/<int:subnet_id>', methods=['PUT']) @app.route('/api/v1/subnets/<int:subnet_id>', methods=['PUT'])
@rate_limit("50 per minute")
@api_permission_required('edit_subnet') @api_permission_required('edit_subnet')
def api_update_subnet(subnet_id): def api_update_subnet(subnet_id):
"""Update a subnet""" """Update a subnet"""
@@ -3216,6 +3240,7 @@ def register_routes(app):
return jsonify({'message': 'Subnet updated successfully', 'subnet': {'id': subnet_id, 'name': new_name, 'cidr': new_cidr, 'site': new_site}}) return jsonify({'message': 'Subnet updated successfully', 'subnet': {'id': subnet_id, 'name': new_name, 'cidr': new_cidr, 'site': new_site}})
@app.route('/api/v1/subnets/<int:subnet_id>', methods=['DELETE']) @app.route('/api/v1/subnets/<int:subnet_id>', methods=['DELETE'])
@rate_limit("50 per minute")
@api_permission_required('delete_subnet') @api_permission_required('delete_subnet')
def api_delete_subnet(subnet_id): def api_delete_subnet(subnet_id):
"""Delete a subnet""" """Delete a subnet"""
@@ -3241,6 +3266,7 @@ def register_routes(app):
# Racks API # Racks API
@app.route('/api/v1/racks', methods=['GET']) @app.route('/api/v1/racks', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_racks') @api_permission_required('view_racks')
def api_racks(): def api_racks():
"""Get all racks""" """Get all racks"""
@@ -3266,6 +3292,7 @@ def register_routes(app):
return jsonify({'racks': racks}) return jsonify({'racks': racks})
@app.route('/api/v1/racks/<int:rack_id>', methods=['GET']) @app.route('/api/v1/racks/<int:rack_id>', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_rack') @api_permission_required('view_rack')
def api_rack(rack_id): def api_rack(rack_id):
"""Get a specific rack""" """Get a specific rack"""
@@ -3288,6 +3315,7 @@ def register_routes(app):
return jsonify(rack) return jsonify(rack)
@app.route('/api/v1/racks', methods=['POST']) @app.route('/api/v1/racks', methods=['POST'])
@rate_limit("50 per minute")
@api_permission_required('add_rack') @api_permission_required('add_rack')
def api_add_rack(): def api_add_rack():
"""Create a new rack""" """Create a new rack"""
@@ -3315,6 +3343,7 @@ def register_routes(app):
return jsonify({'id': rack_id, 'name': name, 'site': site, 'height_u': height_u}), 201 return jsonify({'id': rack_id, 'name': name, 'site': site, 'height_u': height_u}), 201
@app.route('/api/v1/racks/<int:rack_id>', methods=['DELETE']) @app.route('/api/v1/racks/<int:rack_id>', methods=['DELETE'])
@rate_limit("50 per minute")
@api_permission_required('delete_rack') @api_permission_required('delete_rack')
def api_delete_rack(rack_id): def api_delete_rack(rack_id):
"""Delete a rack""" """Delete a rack"""
@@ -3332,6 +3361,7 @@ def register_routes(app):
return jsonify({'message': 'Rack deleted successfully', 'rack': {'id': rack_id, 'name': rack_name}}) return jsonify({'message': 'Rack deleted successfully', 'rack': {'id': rack_id, 'name': rack_name}})
@app.route('/api/v1/racks/<int:rack_id>/devices', methods=['POST']) @app.route('/api/v1/racks/<int:rack_id>/devices', methods=['POST'])
@rate_limit("50 per minute")
@api_permission_required('add_device_to_rack') @api_permission_required('add_device_to_rack')
def api_add_device_to_rack(rack_id): def api_add_device_to_rack(rack_id):
"""Add a device to a rack""" """Add a device to a rack"""
@@ -3416,6 +3446,7 @@ def register_routes(app):
}), 201 }), 201
@app.route('/api/v1/racks/<int:rack_id>/devices/<int:rack_device_id>', methods=['DELETE']) @app.route('/api/v1/racks/<int:rack_id>/devices/<int:rack_device_id>', methods=['DELETE'])
@rate_limit("50 per minute")
@api_permission_required('remove_device_from_rack') @api_permission_required('remove_device_from_rack')
def api_remove_device_from_rack(rack_id, rack_device_id): def api_remove_device_from_rack(rack_id, rack_device_id):
"""Remove a device from a rack""" """Remove a device from a rack"""
@@ -3449,6 +3480,7 @@ def register_routes(app):
# Device Types API # Device Types API
@app.route('/api/v1/device-types', methods=['GET']) @app.route('/api/v1/device-types', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_device_types') @api_permission_required('view_device_types')
def api_device_types(): def api_device_types():
"""Get all device types""" """Get all device types"""
@@ -3461,6 +3493,7 @@ def register_routes(app):
# DHCP API # DHCP API
@app.route('/api/v1/subnets/<int:subnet_id>/dhcp', methods=['GET']) @app.route('/api/v1/subnets/<int:subnet_id>/dhcp', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_dhcp') @api_permission_required('view_dhcp')
def api_get_dhcp(subnet_id): def api_get_dhcp(subnet_id):
"""Get DHCP pools for a subnet""" """Get DHCP pools for a subnet"""
@@ -3472,6 +3505,7 @@ def register_routes(app):
return jsonify({'pools': pools}) return jsonify({'pools': pools})
@app.route('/api/v1/subnets/<int:subnet_id>/dhcp', methods=['POST']) @app.route('/api/v1/subnets/<int:subnet_id>/dhcp', methods=['POST'])
@rate_limit("50 per minute")
@api_permission_required('configure_dhcp') @api_permission_required('configure_dhcp')
def api_configure_dhcp(subnet_id): def api_configure_dhcp(subnet_id):
"""Configure DHCP pools for a subnet""" """Configure DHCP pools for a subnet"""
@@ -3561,6 +3595,7 @@ def register_routes(app):
# Tags API # Tags API
@app.route('/api/v1/tags', methods=['GET']) @app.route('/api/v1/tags', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_tags') @api_permission_required('view_tags')
def api_tags(): def api_tags():
"""Get all tags""" """Get all tags"""
@@ -3575,6 +3610,7 @@ def register_routes(app):
return jsonify({'tags': tags}) return jsonify({'tags': tags})
@app.route('/api/v1/tags', methods=['POST']) @app.route('/api/v1/tags', methods=['POST'])
@rate_limit("50 per minute")
@api_permission_required('add_tag') @api_permission_required('add_tag')
def api_add_tag(): def api_add_tag():
"""Create a new tag""" """Create a new tag"""
@@ -3605,6 +3641,7 @@ def register_routes(app):
return jsonify({'error': 'Tag name already exists'}), 400 return jsonify({'error': 'Tag name already exists'}), 400
@app.route('/api/v1/tags/<int:tag_id>', methods=['GET']) @app.route('/api/v1/tags/<int:tag_id>', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_tags') @api_permission_required('view_tags')
def api_tag(tag_id): def api_tag(tag_id):
"""Get a specific tag""" """Get a specific tag"""
@@ -3627,6 +3664,7 @@ def register_routes(app):
return jsonify(tag) return jsonify(tag)
@app.route('/api/v1/tags/<int:tag_id>', methods=['PUT']) @app.route('/api/v1/tags/<int:tag_id>', methods=['PUT'])
@rate_limit("50 per minute")
@api_permission_required('edit_tag') @api_permission_required('edit_tag')
def api_update_tag(tag_id): def api_update_tag(tag_id):
"""Update a tag""" """Update a tag"""
@@ -3677,6 +3715,7 @@ def register_routes(app):
return jsonify({'error': 'Tag name already exists'}), 400 return jsonify({'error': 'Tag name already exists'}), 400
@app.route('/api/v1/tags/<int:tag_id>', methods=['DELETE']) @app.route('/api/v1/tags/<int:tag_id>', methods=['DELETE'])
@rate_limit("50 per minute")
@api_permission_required('delete_tag') @api_permission_required('delete_tag')
def api_delete_tag(tag_id): def api_delete_tag(tag_id):
"""Delete a tag""" """Delete a tag"""
@@ -3697,6 +3736,7 @@ def register_routes(app):
return jsonify({'message': 'Tag deleted successfully'}) return jsonify({'message': 'Tag deleted successfully'})
@app.route('/api/v1/devices/<int:device_id>/tags', methods=['GET']) @app.route('/api/v1/devices/<int:device_id>/tags', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_device') @api_permission_required('view_device')
def api_device_tags(device_id): def api_device_tags(device_id):
"""Get tags for a specific device""" """Get tags for a specific device"""
@@ -3717,6 +3757,7 @@ def register_routes(app):
return jsonify({'tags': tags}) return jsonify({'tags': tags})
@app.route('/api/v1/devices/<int:device_id>/tags', methods=['POST']) @app.route('/api/v1/devices/<int:device_id>/tags', methods=['POST'])
@rate_limit("50 per minute")
@api_permission_required('assign_device_tag') @api_permission_required('assign_device_tag')
def api_assign_device_tag(device_id): def api_assign_device_tag(device_id):
"""Assign a tag to a device""" """Assign a tag to a device"""
@@ -3752,6 +3793,7 @@ def register_routes(app):
return jsonify({'message': 'Tag assigned successfully'}) return jsonify({'message': 'Tag assigned successfully'})
@app.route('/api/v1/devices/<int:device_id>/tags/<int:tag_id>', methods=['DELETE']) @app.route('/api/v1/devices/<int:device_id>/tags/<int:tag_id>', methods=['DELETE'])
@rate_limit("50 per minute")
@api_permission_required('remove_device_tag') @api_permission_required('remove_device_tag')
def api_remove_device_tag(device_id, tag_id): def api_remove_device_tag(device_id, tag_id):
"""Remove a tag from a device""" """Remove a tag from a device"""
@@ -3782,6 +3824,7 @@ def register_routes(app):
return jsonify({'message': 'Tag removed successfully'}) return jsonify({'message': 'Tag removed successfully'})
@app.route('/api/v1/devices/by-tag/<tag_identifier>', methods=['GET']) @app.route('/api/v1/devices/by-tag/<tag_identifier>', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_devices') @api_permission_required('view_devices')
def api_devices_by_tag(tag_identifier): def api_devices_by_tag(tag_identifier):
"""Get devices by tag name or ID. Use ?format=simple for simplified response.""" """Get devices by tag name or ID. Use ?format=simple for simplified response."""
@@ -3894,6 +3937,7 @@ def register_routes(app):
# Audit Log API # Audit Log API
@app.route('/api/v1/audit', methods=['GET']) @app.route('/api/v1/audit', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_audit') @api_permission_required('view_audit')
def api_audit(): def api_audit():
"""Get audit log entries""" """Get audit log entries"""
@@ -3914,6 +3958,7 @@ def register_routes(app):
# Users API (admin only) # Users API (admin only)
@app.route('/api/v1/users', methods=['GET']) @app.route('/api/v1/users', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_users') @api_permission_required('view_users')
def api_users(): def api_users():
"""Get all users (admin only)""" """Get all users (admin only)"""
@@ -3934,6 +3979,7 @@ def register_routes(app):
# Roles API (admin only) # Roles API (admin only)
@app.route('/api/v1/roles', methods=['GET']) @app.route('/api/v1/roles', methods=['GET'])
@rate_limit("100 per minute")
@api_permission_required('view_users') @api_permission_required('view_users')
def api_roles(): def api_roles():
"""Get all roles (admin only)""" """Get all roles (admin only)"""