**ACLs (Access Control Lists):** - Ajout du type AclConfig dans sources/event.v (allowed_ips CIDR + allowed_tokens) - Chaque source webhook (Gitea, Uptime Kuma, Cron, Generic) supporte les ACLs - Validation IP via X-Forwarded-For / X-Real-IP avant HMAC - Validation Bearer token via Authorization header - Tests: 7 tests ACL (IP exact, CIDR, parse IPv4, ACL vide, IP+token combiné) **Plugin system:** - sources/plugin.v: runner exécutable externe, stdout JSON → Event - Exit 0 = publish, exit ≠ 0 = skip. Timeout configurable - Plugin loop dans server.v (goroutine, toutes les 60s) - Example: scripts/example-plugin-disk.sh (vérifie espace disque) **Docs:** - README.md: ajout source Plugin + section Features complète - ARCHITECTURE.md: flux Plugin, flux ACL, endpoints /metrics /api/silence - ROADMAP.md: Phase 5 → 8/8 complet, ajout v0.6.0 - ntfy-bridge.example.yaml: sections ACLs et Plugins commentées - Version bump: 0.5.0 → 0.6.0
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
module main
|
||||
|
||||
import net.http
|
||||
import sources
|
||||
|
||||
fn test_ip_matches_exact() {
|
||||
assert ip_matches('192.168.30.5', '192.168.30.5')
|
||||
assert !ip_matches('192.168.30.6', '192.168.30.5')
|
||||
}
|
||||
|
||||
fn test_ip_matches_cidr() {
|
||||
assert ip_matches('192.168.30.5', '192.168.0.0/16')
|
||||
assert ip_matches('10.0.0.1', '10.0.0.0/8')
|
||||
assert ip_matches('10.255.255.255', '10.0.0.0/8')
|
||||
assert !ip_matches('11.0.0.1', '10.0.0.0/8')
|
||||
assert ip_matches('192.168.1.100', '192.168.1.0/24')
|
||||
assert !ip_matches('192.168.2.1', '192.168.1.0/24')
|
||||
}
|
||||
|
||||
fn test_parse_ip_valid() {
|
||||
octets := parse_ip('192.168.1.5')!
|
||||
assert octets[0] == 192
|
||||
assert octets[1] == 168
|
||||
assert octets[2] == 1
|
||||
assert octets[3] == 5
|
||||
}
|
||||
|
||||
fn test_parse_ip_invalid() {
|
||||
// parse_ip with invalid input should return an error
|
||||
result := parse_ip('invalid') or { []u8{} }
|
||||
assert result.len == 0
|
||||
|
||||
result2 := parse_ip('1.2.3') or { []u8{} }
|
||||
assert result2.len == 0
|
||||
|
||||
result3 := parse_ip('999.1.2.3') or { []u8{} }
|
||||
assert result3.len == 0
|
||||
}
|
||||
|
||||
fn test_check_acl_no_acl() {
|
||||
mut app := new_test_acl_app()
|
||||
route := WebhookRoute{kind: .generic, index: 0}
|
||||
assert app.check_acl(route, '1.2.3.4', '') == true
|
||||
}
|
||||
|
||||
fn test_check_acl_ip_allow() {
|
||||
mut app := new_test_acl_app()
|
||||
app.cfg.sources.generic << sources.GenericSource{
|
||||
name: 'test-source'
|
||||
webhook_path: '/test'
|
||||
topic: 'test-topic'
|
||||
acl: sources.AclConfig{
|
||||
allowed_ips: ['10.0.0.0/8']
|
||||
}
|
||||
}
|
||||
route := WebhookRoute{kind: .generic, index: 0}
|
||||
|
||||
assert app.check_acl(route, '10.0.0.5', '') == true
|
||||
assert app.check_acl(route, '10.255.0.1', '') == true
|
||||
assert app.check_acl(route, '192.168.1.1', '') == false
|
||||
assert app.check_acl(route, '', '') == false
|
||||
}
|
||||
|
||||
fn test_check_acl_combined_ip_and_token() {
|
||||
mut app := new_test_acl_app()
|
||||
app.cfg.sources.generic << sources.GenericSource{
|
||||
name: 'test-source'
|
||||
webhook_path: '/test'
|
||||
topic: 'test-topic'
|
||||
acl: sources.AclConfig{
|
||||
allowed_ips: ['10.0.0.0/8']
|
||||
allowed_tokens: ['secret123']
|
||||
}
|
||||
}
|
||||
route := WebhookRoute{kind: .generic, index: 0}
|
||||
|
||||
// Both pass
|
||||
assert app.check_acl(route, '10.0.0.5', 'secret123') == true
|
||||
// IP ok but no token
|
||||
assert app.check_acl(route, '10.0.0.5', '') == false
|
||||
// Token ok but wrong IP
|
||||
assert app.check_acl(route, '192.168.1.1', 'secret123') == false
|
||||
// Both wrong
|
||||
assert app.check_acl(route, '1.1.1.1', 'wrong') == false
|
||||
}
|
||||
|
||||
fn test_extract_bearer_token_empty() {
|
||||
req := http.Request{
|
||||
header: http.new_header()
|
||||
}
|
||||
assert extract_bearer_token(req) == ''
|
||||
}
|
||||
|
||||
// Helper: create a minimal App for ACL tests
|
||||
fn new_test_acl_app() App {
|
||||
return App{
|
||||
logger: new_logger(.info, false)
|
||||
cfg: Config{
|
||||
filters: FilterConfig{}
|
||||
outgoing: OutgoingConfig{}
|
||||
state: StateConfig{}
|
||||
}
|
||||
poll_state: map[string]bool{}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user