Add IP whitelist match rule.

This commit is contained in:
HandcraftedBits 2017-02-20 22:00:39 -05:00
parent c8a83349d2
commit 173273b466
3 changed files with 115 additions and 52 deletions

View file

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"net/textproto" "net/textproto"
"reflect" "reflect"
"regexp" "regexp"
@ -100,6 +101,48 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str
return expectedMAC, err return expectedMAC, err
} }
// CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range
// (in CIDR form or a single IP address).
func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) {
// Extract IP address from remote address.
ip := remoteAddr
if strings.LastIndex(remoteAddr, ":") != -1 {
ip = remoteAddr[0:strings.LastIndex(remoteAddr, ":")]
}
ip = strings.TrimSpace(ip)
// IPv6 addresses will likely be surrounded by [], so don't forget to remove those.
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
ip = ip[1 : len(ip)-1]
}
parsedIP := net.ParseIP(strings.TrimSpace(ip))
if parsedIP == nil {
return false, fmt.Errorf("invalid IP address found in remote address '%s'", remoteAddr)
}
// Extract IP range in CIDR form. If a single IP address is provided, turn it into CIDR form.
ipRange = strings.TrimSpace(ipRange)
if strings.Index(ipRange, "/") == -1 {
ipRange = ipRange + "/32"
}
_, cidr, err := net.ParseCIDR(ipRange)
if err != nil {
return false, err
}
return cidr.Contains(parsedIP), nil
}
// ReplaceParameter replaces parameter value with the passed value in the passed map // ReplaceParameter replaces parameter value with the passed value in the passed map
// (please note you should pass pointer to the map, because we're modifying it) // (please note you should pass pointer to the map, because we're modifying it)
// based on the passed string // based on the passed string
@ -479,16 +522,16 @@ type Rules struct {
// Evaluate finds the first rule property that is not nil and returns the value // Evaluate finds the first rule property that is not nil and returns the value
// it evaluates to // it evaluates to
func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) { func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
switch { switch {
case r.And != nil: case r.And != nil:
return r.And.Evaluate(headers, query, payload, body) return r.And.Evaluate(headers, query, payload, body, remoteAddr)
case r.Or != nil: case r.Or != nil:
return r.Or.Evaluate(headers, query, payload, body) return r.Or.Evaluate(headers, query, payload, body, remoteAddr)
case r.Not != nil: case r.Not != nil:
return r.Not.Evaluate(headers, query, payload, body) return r.Not.Evaluate(headers, query, payload, body, remoteAddr)
case r.Match != nil: case r.Match != nil:
return r.Match.Evaluate(headers, query, payload, body) return r.Match.Evaluate(headers, query, payload, body, remoteAddr)
} }
return false, nil return false, nil
@ -498,11 +541,11 @@ func (r Rules) Evaluate(headers, query, payload *map[string]interface{}, body *[
type AndRule []Rules type AndRule []Rules
// Evaluate AndRule will return true if and only if all of ChildRules evaluate to true // Evaluate AndRule will return true if and only if all of ChildRules evaluate to true
func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) { func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
res := true res := true
for _, v := range r { for _, v := range r {
rv, err := v.Evaluate(headers, query, payload, body) rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -520,11 +563,11 @@ func (r AndRule) Evaluate(headers, query, payload *map[string]interface{}, body
type OrRule []Rules type OrRule []Rules
// Evaluate OrRule will return true if any of ChildRules evaluate to true // Evaluate OrRule will return true if any of ChildRules evaluate to true
func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) { func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
res := false res := false
for _, v := range r { for _, v := range r {
rv, err := v.Evaluate(headers, query, payload, body) rv, err := v.Evaluate(headers, query, payload, body, remoteAddr)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -542,8 +585,8 @@ func (r OrRule) Evaluate(headers, query, payload *map[string]interface{}, body *
type NotRule Rules type NotRule Rules
// Evaluate NotRule will return true if and only if ChildRule evaluates to false // Evaluate NotRule will return true if and only if ChildRule evaluates to false
func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) { func (r NotRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
rv, err := Rules(r).Evaluate(headers, query, payload, body) rv, err := Rules(r).Evaluate(headers, query, payload, body, remoteAddr)
return !rv, err return !rv, err
} }
@ -554,6 +597,7 @@ type MatchRule struct {
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
Value string `json:"value,omitempty"` Value string `json:"value,omitempty"`
Parameter Argument `json:"parameter,omitempty"` Parameter Argument `json:"parameter,omitempty"`
IPRange string `json:"ip-range,omitempty"`
} }
// Constants for the MatchRule type // Constants for the MatchRule type
@ -561,10 +605,15 @@ const (
MatchValue string = "value" MatchValue string = "value"
MatchRegex string = "regex" MatchRegex string = "regex"
MatchHashSHA1 string = "payload-hash-sha1" MatchHashSHA1 string = "payload-hash-sha1"
IPWhitelist string = "ip-whitelist"
) )
// Evaluate MatchRule will return based on the type // Evaluate MatchRule will return based on the type
func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte) (bool, error) { func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, body *[]byte, remoteAddr string) (bool, error) {
if r.Type == IPWhitelist {
return CheckIPWhitelist(remoteAddr, r.IPRange)
}
if arg, ok := r.Parameter.Get(headers, query, payload); ok { if arg, ok := r.Parameter.Get(headers, query, payload); ok {
switch r.Type { switch r.Type {
case MatchValue: case MatchValue:

View file

@ -241,29 +241,43 @@ func TestHooksMatch(t *testing.T) {
} }
var matchRuleTests = []struct { var matchRuleTests = []struct {
typ, regex, secret, value string typ, regex, secret, value, ipRange string
param Argument param Argument
headers, query, payload *map[string]interface{} headers, query, payload *map[string]interface{}
body []byte body []byte
ok bool remoteAddr string
err bool ok bool
err bool
}{ }{
{"value", "", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, {"value", "", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"regex", "^z", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, {"regex", "^z", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false},
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false}, {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false},
// failures // failures
{"value", "", "", "X", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false}, {"value", "", "", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"regex", "^X", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false}, {"regex", "^X", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false},
{"value", "", "2", "X", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header {"value", "", "2", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, false}, // reference invalid header
// errors // errors
{"regex", "*", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, true}, // invalid regex {"regex", "*", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac
// IP whitelisting, valid cases
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.1", Argument{}, nil, nil, nil, []byte{}, "192.168.0.1:9000", true, false}, // valid IPv4, no range
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, with range
{"ip-whitelist", "", "", "", "::1", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", true, false}, // valid IPv6, no range
// IP whitelisting, invalid cases
{"ip-whitelist", "", "", "", "192.168.0.1/a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, with range
{"ip-whitelist", "", "", "", "192.168.0.a", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", false, true}, // invalid IPv4, no range
{"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.a:9000", false, true}, // invalid IPv4 address
{"ip-whitelist", "", "", "", "::1/a", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, with range
{"ip-whitelist", "", "", "", "::z", Argument{}, nil, nil, nil, []byte{}, "[::1]:9000", false, true}, // invalid IPv6, no range
{"ip-whitelist", "", "", "", "::1/24", Argument{}, nil, nil, nil, []byte{}, "[::z]:9000", false, true}, // invalid IPv6 address
} }
func TestMatchRule(t *testing.T) { func TestMatchRule(t *testing.T) {
for i, tt := range matchRuleTests { for i, tt := range matchRuleTests {
r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param} r := MatchRule{tt.typ, tt.regex, tt.secret, tt.value, tt.param, tt.ipRange}
ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body) ok, err := r.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, tt.remoteAddr)
if ok != tt.ok || (err != nil) != tt.err { if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, (err != nil)) t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, (err != nil))
} }
@ -281,8 +295,8 @@ var andRuleTests = []struct {
{ {
"(a=z, b=y): a=z && b=y", "(a=z, b=y): a=z && b=y",
AndRule{ AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
}, },
&map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{}, &map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{},
true, false, true, false,
@ -290,8 +304,8 @@ var andRuleTests = []struct {
{ {
"(a=z, b=Y): a=z && b=y", "(a=z, b=Y): a=z && b=y",
AndRule{ AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
}, },
&map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{}, &map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{},
false, false, false, false,
@ -300,22 +314,22 @@ var andRuleTests = []struct {
{ {
"(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u", "(a=z, b=y, c=x, d=w=, e=X, f=X): a=z && (b=y && c=x) && (d=w || e=v) && !f=u",
AndRule{ AndRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{ {
And: &AndRule{ And: &AndRule{
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
{Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}}}, {Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", ""}, ""}},
}, },
}, },
{ {
Or: &OrRule{ Or: &OrRule{
{Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}}}, {Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", ""}, ""}},
{Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}}}, {Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", ""}, ""}},
}, },
}, },
{ {
Not: &NotRule{ Not: &NotRule{
Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}}, Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", ""}, ""},
}, },
}, },
}, },
@ -326,7 +340,7 @@ var andRuleTests = []struct {
// failures // failures
{ {
"invalid rule", "invalid rule",
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}}, AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}},
&map[string]interface{}{"Y": "z"}, nil, nil, nil, &map[string]interface{}{"Y": "z"}, nil, nil, nil,
false, false, false, false,
}, },
@ -334,7 +348,7 @@ var andRuleTests = []struct {
func TestAndRule(t *testing.T) { func TestAndRule(t *testing.T) {
for _, tt := range andRuleTests { for _, tt := range andRuleTests {
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body) ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
if ok != tt.ok || (err != nil) != tt.err { if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err) t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.desc, tt.ok, tt.err, ok, err)
} }
@ -352,8 +366,8 @@ var orRuleTests = []struct {
{ {
"(a=z, b=X): a=z || b=y", "(a=z, b=X): a=z || b=y",
OrRule{ OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
}, },
&map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{}, &map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{},
true, false, true, false,
@ -361,8 +375,8 @@ var orRuleTests = []struct {
{ {
"(a=X, b=y): a=z || b=y", "(a=X, b=y): a=z || b=y",
OrRule{ OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
}, },
&map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{}, &map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{},
true, false, true, false,
@ -370,8 +384,8 @@ var orRuleTests = []struct {
{ {
"(a=Z, b=Y): a=z || b=y", "(a=Z, b=Y): a=z || b=y",
OrRule{ OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
{Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}}}, {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", ""}, ""}},
}, },
&map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{}, &map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{},
false, false, false, false,
@ -380,7 +394,7 @@ var orRuleTests = []struct {
{ {
"invalid rule", "invalid rule",
OrRule{ OrRule{
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}},
}, },
&map[string]interface{}{"Y": "Z"}, nil, nil, []byte{}, &map[string]interface{}{"Y": "Z"}, nil, nil, []byte{},
false, false, false, false,
@ -389,7 +403,7 @@ var orRuleTests = []struct {
func TestOrRule(t *testing.T) { func TestOrRule(t *testing.T) {
for _, tt := range orRuleTests { for _, tt := range orRuleTests {
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body) ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
if ok != tt.ok || (err != nil) != tt.err { if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err) t.Errorf("%#v:\nexpected ok: %#v, err: %v\ngot ok: %#v err: %v", tt.desc, tt.ok, tt.err, ok, err)
} }
@ -404,13 +418,13 @@ var notRuleTests = []struct {
ok bool ok bool
err bool err bool
}{ }{
{"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, {"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", ""}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false},
{"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false}, {"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", ""}, ""}}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false},
} }
func TestNotRule(t *testing.T) { func TestNotRule(t *testing.T) {
for _, tt := range notRuleTests { for _, tt := range notRuleTests {
ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body) ok, err := tt.rule.Evaluate(tt.headers, tt.query, tt.payload, &tt.body, "")
if ok != tt.ok || (err != nil) != tt.err { if ok != tt.ok || (err != nil) != tt.err {
t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err) t.Errorf("failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", tt.rule, tt.ok, tt.err, ok, err)
} }

View file

@ -243,7 +243,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
if matchedHook.TriggerRule == nil { if matchedHook.TriggerRule == nil {
ok = true ok = true
} else { } else {
ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body) ok, err = matchedHook.TriggerRule.Evaluate(&headers, &query, &payload, &body, r.RemoteAddr)
if err != nil { if err != nil {
msg := fmt.Sprintf("error evaluating hook: %s", err) msg := fmt.Sprintf("error evaluating hook: %s", err)
log.Printf(msg) log.Printf(msg)