diff --git a/docs/Referencing-Request-Values.md b/docs/Referencing-Request-Values.md index ba353637..96c949e3 100644 --- a/docs/Referencing-Request-Values.md +++ b/docs/Referencing-Request-Values.md @@ -111,6 +111,16 @@ If you are referencing values for environment, you can use `envname` property to ``` to get the QUERY environment variable set to the `q` parameter passed in the query string. +If you need a default value, you can use `default` property to set a value used in case the value is not found or is empty like so +```json +{ + "source": "url", + "name": "q", + "default": "123" +} +``` +**Note**: The `default` property is ignored when the `source` is set to `string`, `entire-payload`, `entire-headers` or `entire-query`. + # Special cases If you want to pass the entire payload as JSON string to your command you can use ```json diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 394dd799..04927e21 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -355,7 +355,7 @@ func ReplaceParameter(s string, params, value interface{}) bool { } // GetParameter extracts interface{} value based on the passed string -func GetParameter(s string, params interface{}) (interface{}, error) { +func GetParameter(s string, params interface{}, defaultValue string) (interface{}, error) { if params == nil { return nil, errors.New("no parameters") } @@ -374,45 +374,70 @@ func GetParameter(s string, params interface{}) (interface{}, error) { return nil, &ParameterNodeError{s} } - return GetParameter(p[1], params.([]interface{})[index]) + return GetParameter(p[1], params.([]interface{})[index], defaultValue) } index, err := strconv.ParseUint(s, 10, 64) if err != nil || paramsValueSliceLength <= int(index) { + // Return default value if nothing found and defaultValue is set + if defaultValue != "" { + return defaultValue, nil + } + return nil, &ParameterNodeError{s} } return params.([]interface{})[index], nil } + // Return default value if nothing found and defaultValue is set + if defaultValue != "" { + return defaultValue, nil + } + return nil, &ParameterNodeError{s} case reflect.Map: // Check for raw key if v, ok := params.(map[string]interface{})[s]; ok { + // Return default value if found value is empty and defaultValue is set + if (v == "" || v == nil) && defaultValue != "" { + return defaultValue, nil + } + return v, nil } + // Return default value if no dot inside the string and defaultValue is set + if !strings.Contains(s, ".") && defaultValue != "" { + return defaultValue, nil + } + // Checked for dotted references p := strings.SplitN(s, ".", 2) if pValue, ok := params.(map[string]interface{})[p[0]]; ok { if len(p) > 1 { - return GetParameter(p[1], pValue) + return GetParameter(p[1], pValue, defaultValue) } return pValue, nil } } + // Return default value if nothing found and defaultValue is set + if defaultValue != "" { + return defaultValue, nil + } + return nil, &ParameterNodeError{s} } // ExtractParameterAsString extracts value from interface{} as string based on // the passed string. Complex data types are rendered as JSON instead of the Go // Stringer format. -func ExtractParameterAsString(s string, params interface{}) (string, error) { - pValue, err := GetParameter(s, params) +func ExtractParameterAsString(s string, params interface{}, defaultValue string) (string, error) { + pValue, err := GetParameter(s, params, defaultValue) if err != nil { return "", err } @@ -436,6 +461,7 @@ func ExtractParameterAsString(s string, params interface{}) (string, error) { type Argument struct { Source string `json:"source,omitempty"` Name string `json:"name,omitempty"` + DefaultValue string `json:"default,omitempty"` EnvName string `json:"envname,omitempty"` Base64Decode bool `json:"base64decode,omitempty"` } @@ -503,7 +529,7 @@ func (ha *Argument) Get(r *Request) (string, error) { } if source != nil { - return ExtractParameterAsString(key, *source) + return ExtractParameterAsString(key, *source, ha.DefaultValue) } return "", errors.New("no source for value retrieval") diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go index cbc49f70..a2671a47 100644 --- a/internal/hook/hook_test.go +++ b/internal/hook/hook_test.go @@ -29,7 +29,7 @@ func TestGetParameter(t *testing.T) { {"z.b", map[string]interface{}{"a": map[string]interface{}{"z": 2}}, nil, false}, {"a.2", map[string]interface{}{"a": []interface{}{"a", "b"}}, nil, false}, } { - res, err := GetParameter(test.key, test.val) + res, err := GetParameter(test.key, test.val, "") if (err == nil) != test.ok { t.Errorf("unexpected result given {%q, %q}: %s\n", test.key, test.val, err) } @@ -245,7 +245,7 @@ var extractParameterTests = []struct { func TestExtractParameter(t *testing.T) { for _, tt := range extractParameterTests { - value, err := ExtractParameterAsString(tt.s, tt.params) + value, err := ExtractParameterAsString(tt.s, tt.params, "") if (err == nil) != tt.ok || value != tt.value { t.Errorf("failed to extract parameter %q:\nexpected {value:%#v, ok:%#v},\ngot {value:%#v, err:%v}", tt.s, tt.value, tt.ok, value, err) } @@ -274,7 +274,7 @@ var argumentGetTests = []struct { func TestArgumentGet(t *testing.T) { for _, tt := range argumentGetTests { - a := Argument{tt.source, tt.name, "", false} + a := Argument{tt.source, tt.name, "", "", false} r := &Request{ Headers: tt.headers, Query: tt.query, @@ -294,14 +294,14 @@ var hookParseJSONParametersTests = []struct { rheaders, rquery, rpayload map[string]interface{} ok bool }{ - {[]Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true}, - {[]Argument{Argument{"url", "a", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true}, - {[]Argument{Argument{"payload", "a", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true}, - {[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true}, + {[]Argument{Argument{"header", "a", "", "", false}}, map[string]interface{}{"A": `{"b": "y"}`}, nil, nil, map[string]interface{}{"A": map[string]interface{}{"b": "y"}}, nil, nil, true}, + {[]Argument{Argument{"url", "a", "", "", false}}, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, nil, true}, + {[]Argument{Argument{"payload", "a", "", "", false}}, nil, nil, map[string]interface{}{"a": `{"b": "y"}`}, nil, nil, map[string]interface{}{"a": map[string]interface{}{"b": "y"}}, true}, + {[]Argument{Argument{"header", "z", "", "", false}}, map[string]interface{}{"Z": `{}`}, nil, nil, map[string]interface{}{"Z": map[string]interface{}{}}, nil, nil, true}, // failures - {[]Argument{Argument{"header", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string - {[]Argument{Argument{"header", "y", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter - {[]Argument{Argument{"string", "z", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source + {[]Argument{Argument{"header", "z", "", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // empty string + {[]Argument{Argument{"header", "y", "", "", false}}, map[string]interface{}{"X": `{}`}, nil, nil, map[string]interface{}{"X": `{}`}, nil, nil, false}, // missing parameter + {[]Argument{Argument{"string", "z", "", "", false}}, map[string]interface{}{"Z": ``}, nil, nil, map[string]interface{}{"Z": ``}, nil, nil, false}, // invalid argument source } func TestHookParseJSONParameters(t *testing.T) { @@ -326,9 +326,9 @@ var hookExtractCommandArgumentsTests = []struct { value []string ok bool }{ - {"test", []Argument{Argument{"header", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true}, + {"test", []Argument{Argument{"header", "a", "", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"test", "z"}, true}, // failures - {"fail", []Argument{Argument{"payload", "a", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false}, + {"fail", []Argument{Argument{"payload", "a", "", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"fail", ""}, false}, } func TestHookExtractCommandArguments(t *testing.T) { @@ -375,14 +375,14 @@ var hookExtractCommandArgumentsForEnvTests = []struct { // successes { "test", - []Argument{Argument{"header", "a", "", false}}, + []Argument{Argument{"header", "a", "", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"HOOK_a=z"}, true, }, { "test", - []Argument{Argument{"header", "a", "MYKEY", false}}, + []Argument{Argument{"header", "a", "", "MYKEY", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{"MYKEY=z"}, true, @@ -390,7 +390,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct { // failures { "fail", - []Argument{Argument{"payload", "a", "", false}}, + []Argument{Argument{"payload", "a", "", "", false}}, map[string]interface{}{"A": "z"}, nil, nil, []string{}, false, @@ -489,24 +489,24 @@ var matchRuleTests = []struct { ok bool err bool }{ - {"value", "", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, - {"regex", "^z", "", "z", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, - {"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, - {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, - {"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, - {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, + {"value", "", "", "z", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, + {"regex", "^z", "", "z", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, + {"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, + {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, + {"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, + {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, // failures - {"value", "", "", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, - {"regex", "^X", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, - {"value", "", "2", "X", "", Argument{"header", "a", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header + {"value", "", "", "X", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, + {"regex", "^X", "", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, + {"value", "", "2", "X", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header // errors - {"regex", "*", "", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex - {"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"regex", "*", "", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex + {"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", "", false}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", "", false}, 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 @@ -552,8 +552,8 @@ var andRuleTests = []struct { { "(a=z, b=y): a=z && b=y", AndRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", "", false}, ""}}, }, map[string]interface{}{"A": "z", "B": "y"}, nil, nil, []byte{}, @@ -562,8 +562,8 @@ var andRuleTests = []struct { { "(a=z, b=Y): a=z && b=y", AndRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", "", false}, ""}}, }, map[string]interface{}{"A": "z", "B": "Y"}, nil, nil, []byte{}, @@ -573,22 +573,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", AndRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", "", false}, ""}}, { And: &AndRule{ - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}}, - {Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "x", Argument{"header", "c", "", "", false}, ""}}, }, }, { Or: &OrRule{ - {Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", false}, ""}}, - {Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "w", Argument{"header", "d", "", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "v", Argument{"header", "e", "", "", false}, ""}}, }, }, { Not: &NotRule{ - Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", false}, ""}, + Match: &MatchRule{"value", "", "", "u", Argument{"header", "f", "", "", false}, ""}, }, }, }, @@ -600,7 +600,7 @@ var andRuleTests = []struct { // failures { "invalid rule", - AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}}, + AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", "", false}, ""}}}, map[string]interface{}{"Y": "z"}, nil, nil, nil, false, true, }, @@ -632,8 +632,8 @@ var orRuleTests = []struct { { "(a=z, b=X): a=z || b=y", OrRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", "", false}, ""}}, }, map[string]interface{}{"A": "z", "B": "X"}, nil, nil, []byte{}, @@ -642,8 +642,8 @@ var orRuleTests = []struct { { "(a=X, b=y): a=z || b=y", OrRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", "", false}, ""}}, }, map[string]interface{}{"A": "X", "B": "y"}, nil, nil, []byte{}, @@ -652,8 +652,8 @@ var orRuleTests = []struct { { "(a=Z, b=Y): a=z || b=y", OrRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, - {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "y", Argument{"header", "b", "", "", false}, ""}}, }, map[string]interface{}{"A": "Z", "B": "Y"}, nil, nil, []byte{}, @@ -663,7 +663,7 @@ var orRuleTests = []struct { { "missing parameter node", OrRule{ - {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, + {Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", "", false}, ""}}, }, map[string]interface{}{"Y": "Z"}, nil, nil, []byte{}, @@ -694,8 +694,8 @@ var notRuleTests = []struct { ok bool err bool }{ - {"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, - {"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false}, + {"(a=z): !a=X", NotRule{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a", "", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, + {"(a=z): !a=z", NotRule{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a", "", "", false}, ""}}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, false, false}, } func TestNotRule(t *testing.T) {