Add IP Whitelist Plugin and corresponding tests
- Introduced `IpWhitelistPlugin` (DOMAIN): Restricts access to specific IP addresses or CIDR ranges. - Added configuration options: `enabled`, `allowed_ips`, and `status_code`. - Updated `plugins.md` and `plugin-development.md` to document `IpWhitelistPlugin`. - Created fixtures for `services-with-ip-whitelist`. - Added extensive test cases for `IpWhitelistPlugin` to validate configuration and HAProxy output generation. - Ensured seamless integration into the plugin framework alongside other domain plugins.
This commit is contained in:
parent
91f8ff5d08
commit
57387f3e32
5 changed files with 1549 additions and 428 deletions
107
src/plugins/builtin/ip_whitelist.py
Normal file
107
src/plugins/builtin/ip_whitelist.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""
|
||||
IP Whitelist Plugin for EasyHAProxy
|
||||
|
||||
This plugin restricts access to a domain to only specific IP addresses or CIDR ranges.
|
||||
It runs as a DOMAIN plugin (once per domain).
|
||||
|
||||
Configuration:
|
||||
- enabled: Enable/disable the plugin (default: true)
|
||||
- allowed_ips: Comma-separated list of IPs/CIDR ranges to allow
|
||||
- status_code: HTTP status code to return for blocked IPs (default: 403)
|
||||
|
||||
Example YAML config:
|
||||
plugins:
|
||||
ip_whitelist:
|
||||
enabled: true
|
||||
allowed_ips: "192.168.1.0/24,10.0.0.1,172.16.0.0/16"
|
||||
status_code: 403
|
||||
|
||||
Example Container Label:
|
||||
easyhaproxy.http.plugins: "ip_whitelist"
|
||||
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: "192.168.1.0/24,10.0.0.1"
|
||||
easyhaproxy.http.plugin.ip_whitelist.status_code: 403
|
||||
|
||||
HAProxy Config Generated:
|
||||
# IP Whitelist - Only allow specific IPs
|
||||
acl whitelisted_ip src 192.168.1.0/24 10.0.0.1
|
||||
http-request deny deny_status 403 if !whitelisted_ip
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||
|
||||
|
||||
class IpWhitelistPlugin(PluginInterface):
|
||||
"""Plugin to restrict access to specific IP addresses"""
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = True
|
||||
self.allowed_ips = []
|
||||
self.status_code = 403
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "ip_whitelist"
|
||||
|
||||
@property
|
||||
def plugin_type(self) -> PluginType:
|
||||
return PluginType.DOMAIN
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
"""
|
||||
Configure the plugin
|
||||
|
||||
Args:
|
||||
config: Dictionary with configuration options
|
||||
- enabled: Whether plugin is enabled
|
||||
- allowed_ips: Comma-separated list of IPs/CIDR ranges
|
||||
- status_code: HTTP status code to return for denied requests
|
||||
"""
|
||||
if "enabled" in config:
|
||||
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||
|
||||
if "allowed_ips" in config:
|
||||
ips_str = str(config["allowed_ips"])
|
||||
self.allowed_ips = [ip.strip() for ip in ips_str.split(",") if ip.strip()]
|
||||
|
||||
if "status_code" in config:
|
||||
try:
|
||||
self.status_code = int(config["status_code"])
|
||||
except ValueError:
|
||||
self.status_code = 403
|
||||
|
||||
def process(self, context: PluginContext) -> PluginResult:
|
||||
"""
|
||||
Generate HAProxy config to whitelist specific IPs
|
||||
|
||||
Args:
|
||||
context: Plugin execution context with domain information
|
||||
|
||||
Returns:
|
||||
PluginResult with HAProxy configuration snippet
|
||||
"""
|
||||
if not self.enabled or not self.allowed_ips:
|
||||
return PluginResult()
|
||||
|
||||
# Create space-separated list of IPs for ACL
|
||||
ips_str = " ".join(self.allowed_ips)
|
||||
|
||||
# Generate HAProxy config snippet
|
||||
haproxy_config = f"""# IP Whitelist - Only allow specific IPs
|
||||
acl whitelisted_ip src {ips_str}
|
||||
http-request deny deny_status {self.status_code} if !whitelisted_ip"""
|
||||
|
||||
return PluginResult(
|
||||
haproxy_config=haproxy_config,
|
||||
modified_easymapping=None,
|
||||
metadata={
|
||||
"domain": context.domain,
|
||||
"allowed_ips": self.allowed_ips,
|
||||
"status_code": self.status_code
|
||||
}
|
||||
)
|
||||
10
src/tests/fixtures/services-with-ip-whitelist
vendored
Normal file
10
src/tests/fixtures/services-with-ip-whitelist
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"192.168.1.40": {
|
||||
"easyhaproxy.http.host": "secure.example.com",
|
||||
"easyhaproxy.http.port": "80",
|
||||
"easyhaproxy.http.localport": "8080",
|
||||
"easyhaproxy.http.plugins": "ip_whitelist",
|
||||
"easyhaproxy.http.plugin.ip_whitelist.allowed_ips": "192.168.1.0/24,10.0.0.5",
|
||||
"easyhaproxy.http.plugin.ip_whitelist.status_code": "403"
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ from plugins import PluginManager, PluginContext
|
|||
from plugins.builtin.cloudflare import CloudflarePlugin
|
||||
from plugins.builtin.cleanup import CleanupPlugin
|
||||
from plugins.builtin.deny_pages import DenyPagesPlugin
|
||||
from plugins.builtin.ip_whitelist import IpWhitelistPlugin
|
||||
import easymapping
|
||||
|
||||
|
||||
|
|
@ -320,6 +321,110 @@ class TestDenyPagesPlugin:
|
|||
assert "http-request deny deny_status 404 if denied_path" in haproxy_config
|
||||
|
||||
|
||||
class TestIpWhitelistPlugin:
|
||||
"""Test cases for IpWhitelistPlugin (DOMAIN plugin)"""
|
||||
|
||||
def test_ip_whitelist_plugin_initialization(self):
|
||||
"""Test plugin initializes with correct defaults"""
|
||||
plugin = IpWhitelistPlugin()
|
||||
assert plugin.name == "ip_whitelist"
|
||||
assert plugin.enabled is True
|
||||
assert plugin.allowed_ips == []
|
||||
assert plugin.status_code == 403
|
||||
|
||||
def test_ip_whitelist_plugin_configuration(self):
|
||||
"""Test plugin configuration"""
|
||||
plugin = IpWhitelistPlugin()
|
||||
|
||||
# Test allowed IPs
|
||||
plugin.configure({"allowed_ips": "192.168.1.0/24,10.0.0.1,172.16.0.0/16"})
|
||||
assert plugin.allowed_ips == ["192.168.1.0/24", "10.0.0.1", "172.16.0.0/16"]
|
||||
|
||||
# Test status code
|
||||
plugin.configure({"status_code": "404"})
|
||||
assert plugin.status_code == 404
|
||||
|
||||
# Test enabled
|
||||
plugin.configure({"enabled": "false"})
|
||||
assert plugin.enabled is False
|
||||
|
||||
def test_ip_whitelist_plugin_generates_config(self):
|
||||
"""Test plugin generates correct HAProxy config"""
|
||||
plugin = IpWhitelistPlugin()
|
||||
plugin.configure({
|
||||
"allowed_ips": "192.168.1.0/24,10.0.0.1",
|
||||
"status_code": "403"
|
||||
})
|
||||
|
||||
context = PluginContext(
|
||||
parsed_object={},
|
||||
easymapping=[],
|
||||
container_env={},
|
||||
domain="example.com",
|
||||
port="80",
|
||||
host_config={}
|
||||
)
|
||||
|
||||
result = plugin.process(context)
|
||||
|
||||
assert result.haproxy_config is not None
|
||||
assert "IP Whitelist" in result.haproxy_config
|
||||
assert "acl whitelisted_ip src 192.168.1.0/24 10.0.0.1" in result.haproxy_config
|
||||
assert "http-request deny deny_status 403 if !whitelisted_ip" in result.haproxy_config
|
||||
assert result.metadata["domain"] == "example.com"
|
||||
assert result.metadata["allowed_ips"] == ["192.168.1.0/24", "10.0.0.1"]
|
||||
assert result.metadata["status_code"] == 403
|
||||
|
||||
def test_ip_whitelist_plugin_disabled(self):
|
||||
"""Test plugin returns empty config when disabled"""
|
||||
plugin = IpWhitelistPlugin()
|
||||
plugin.configure({"enabled": "false", "allowed_ips": "192.168.1.0/24"})
|
||||
|
||||
context = PluginContext(
|
||||
parsed_object={},
|
||||
easymapping=[],
|
||||
container_env={},
|
||||
domain="example.com"
|
||||
)
|
||||
|
||||
result = plugin.process(context)
|
||||
assert result.haproxy_config == ""
|
||||
assert result.metadata == {}
|
||||
|
||||
def test_ip_whitelist_plugin_no_ips(self):
|
||||
"""Test plugin returns empty config when no IPs configured"""
|
||||
plugin = IpWhitelistPlugin()
|
||||
|
||||
context = PluginContext(
|
||||
parsed_object={},
|
||||
easymapping=[],
|
||||
container_env={},
|
||||
domain="example.com"
|
||||
)
|
||||
|
||||
result = plugin.process(context)
|
||||
assert result.haproxy_config == ""
|
||||
|
||||
def test_ip_whitelist_plugin_in_haproxy_config(self):
|
||||
"""Test IP Whitelist plugin integration in full HAProxy config generation"""
|
||||
# Use fixture with ip_whitelist plugin enabled via labels
|
||||
line_list = load_fixture("services-with-ip-whitelist")
|
||||
|
||||
result = {
|
||||
"customerrors": False,
|
||||
"certbot": {"email": "test@example.com"},
|
||||
"stats": {"port": 0}
|
||||
}
|
||||
|
||||
cfg = easymapping.HaproxyConfigGenerator(result)
|
||||
haproxy_config = cfg.generate(line_list)
|
||||
|
||||
# Verify IP Whitelist config is in the output
|
||||
assert "IP Whitelist - Only allow specific IPs" in haproxy_config
|
||||
assert "acl whitelisted_ip src 192.168.1.0/24 10.0.0.5" in haproxy_config
|
||||
assert "http-request deny deny_status 403 if !whitelisted_ip" in haproxy_config
|
||||
|
||||
|
||||
class TestPluginManager:
|
||||
"""Test cases for PluginManager"""
|
||||
|
||||
|
|
@ -332,15 +437,17 @@ class TestPluginManager:
|
|||
assert "cloudflare" in manager.plugins
|
||||
assert "cleanup" in manager.plugins
|
||||
assert "deny_pages" in manager.plugins
|
||||
assert "ip_whitelist" in manager.plugins
|
||||
|
||||
# Verify plugin types
|
||||
assert len(manager.global_plugins) == 1 # cleanup
|
||||
assert len(manager.domain_plugins) == 2 # cloudflare, deny_pages
|
||||
assert len(manager.domain_plugins) == 3 # cloudflare, deny_pages, ip_whitelist
|
||||
|
||||
# Verify plugin instances
|
||||
assert manager.plugins["cloudflare"].name == "cloudflare"
|
||||
assert manager.plugins["cleanup"].name == "cleanup"
|
||||
assert manager.plugins["deny_pages"].name == "deny_pages"
|
||||
assert manager.plugins["ip_whitelist"].name == "ip_whitelist"
|
||||
|
||||
def test_plugin_manager_executes_global_plugins(self):
|
||||
"""Test plugin manager executes global plugins correctly"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue