Browse code

WIP Fix bug 1373330 Invalid formatted generic webhook can trigger new-build without warning

Jim Minter authored on 2016/09/26 19:52:53
Showing 5 changed files
... ...
@@ -51,17 +51,17 @@ func (w *WebHook) ServeHTTP(writer http.ResponseWriter, req *http.Request, ctx k
51 51
 	}
52 52
 
53 53
 	revision, envvars, proceed, err := plugin.Extract(config, secret, "", req)
54
-	switch err {
55
-	case webhook.ErrSecretMismatch, webhook.ErrHookNotEnabled:
56
-		return errors.NewUnauthorized(fmt.Sprintf("the webhook %q for %q did not accept your secret", hookType, name))
57
-	case nil:
58
-	default:
59
-		return errors.NewInternalError(fmt.Errorf("hook failed: %v", err))
60
-	}
61
-
62 54
 	if !proceed {
63
-		return nil
55
+		switch err {
56
+		case webhook.ErrSecretMismatch, webhook.ErrHookNotEnabled:
57
+			return errors.NewUnauthorized(fmt.Sprintf("the webhook %q for %q did not accept your secret", hookType, name))
58
+		case nil:
59
+			return nil
60
+		default:
61
+			return errors.NewInternalError(fmt.Errorf("hook failed: %v", err))
62
+		}
64 63
 	}
64
+	warning := err
65 65
 
66 66
 	buildTriggerCauses := generateBuildTriggerInfo(revision, hookType, secret)
67 67
 	request := &buildapi.BuildRequest{
... ...
@@ -73,7 +73,7 @@ func (w *WebHook) ServeHTTP(writer http.ResponseWriter, req *http.Request, ctx k
73 73
 	if _, err := w.instantiator.Instantiate(config.Namespace, request); err != nil {
74 74
 		return errors.NewInternalError(fmt.Errorf("could not generate a build: %v", err))
75 75
 	}
76
-	return nil
76
+	return warning
77 77
 }
78 78
 
79 79
 func generateBuildTriggerInfo(revision *buildapi.SourceRevision, hookType, secret string) (buildTriggerCauses []buildapi.BuildTriggerCause) {
... ...
@@ -36,18 +36,19 @@ type plugin struct {
36 36
 	Secret, Path string
37 37
 	Err          error
38 38
 	Env          []kapi.EnvVar
39
+	Proceed      bool
39 40
 }
40 41
 
41 42
 func (p *plugin) Extract(buildCfg *api.BuildConfig, secret, path string, req *http.Request) (*api.SourceRevision, []kapi.EnvVar, bool, error) {
42 43
 	p.Secret, p.Path = secret, path
43
-	return nil, p.Env, true, p.Err
44
+	return nil, p.Env, p.Proceed, p.Err
44 45
 }
45 46
 
46 47
 func newStorage() (*rest.WebHook, *buildConfigInstantiator, *test.BuildConfigRegistry) {
47 48
 	mockRegistry := &test.BuildConfigRegistry{}
48 49
 	bci := &buildConfigInstantiator{}
49 50
 	hook := NewWebHookREST(mockRegistry, bci, map[string]webhook.Plugin{
50
-		"ok": &plugin{},
51
+		"ok": &plugin{Proceed: true},
51 52
 		"okenv": &plugin{
52 53
 			Env: []kapi.EnvVar{
53 54
 				{
... ...
@@ -55,6 +56,7 @@ func newStorage() (*rest.WebHook, *buildConfigInstantiator, *test.BuildConfigReg
55 55
 					Value: "bar",
56 56
 				},
57 57
 			},
58
+			Proceed: true,
58 59
 		},
59 60
 		"errsecret": &plugin{Err: webhook.ErrSecretMismatch},
60 61
 		"errhook":   &plugin{Err: webhook.ErrHookNotEnabled},
... ...
@@ -239,7 +241,7 @@ func (p *pathPlugin) Extract(buildCfg *api.BuildConfig, secret, path string, req
239 239
 type errPlugin struct{}
240 240
 
241 241
 func (*errPlugin) Extract(buildCfg *api.BuildConfig, secret, path string, req *http.Request) (*api.SourceRevision, []kapi.EnvVar, bool, error) {
242
-	return nil, []kapi.EnvVar{}, true, errors.New("Plugin error!")
242
+	return nil, []kapi.EnvVar{}, false, errors.New("Plugin error!")
243 243
 }
244 244
 
245 245
 var testBuildConfig = &api.BuildConfig{
... ...
@@ -42,68 +42,74 @@ func (p *WebHookPlugin) Extract(buildCfg *api.BuildConfig, secret, path string,
42 42
 		return revision, envvars, false, err
43 43
 	}
44 44
 
45
-	if buildCfg.Spec.Source.Git == nil {
46
-		glog.V(4).Infof("No git source defined for BuildConfig %s/%s, but triggering anyway", buildCfg.Namespace, buildCfg.Name)
47
-		return revision, envvars, true, err
48
-	}
49
-
50 45
 	contentType := req.Header.Get("Content-Type")
51 46
 	if len(contentType) != 0 {
52 47
 		contentType, _, err = mime.ParseMediaType(contentType)
53 48
 		if err != nil {
54
-			return nil, envvars, false, fmt.Errorf("non-parseable Content-Type %s (%s)", contentType, err)
49
+			return revision, envvars, false, fmt.Errorf("error parsing Content-Type: %s", err)
55 50
 		}
56 51
 	}
57 52
 
58
-	if req.Body != nil && (contentType == "application/json" || contentType == "application/yaml") {
59
-		body, err := ioutil.ReadAll(req.Body)
60
-		if err != nil {
61
-			return nil, envvars, false, err
62
-		}
53
+	if req.Body == nil {
54
+		return revision, envvars, true, nil
55
+	}
63 56
 
64
-		if len(body) == 0 {
65
-			return nil, envvars, true, nil
66
-		}
57
+	if contentType != "application/json" && contentType != "application/yaml" {
58
+		warning := webhook.NewWarning("invalid Content-Type on payload, ignoring payload and continuing with build")
59
+		return revision, envvars, true, warning
60
+	}
67 61
 
68
-		var data api.GenericWebHookEvent
69
-		if contentType == "application/yaml" {
70
-			body, err = yaml.ToJSON(body)
71
-			if err != nil {
72
-				glog.V(4).Infof("Error converting payload to json %v, but continuing with build", err)
73
-				return nil, envvars, true, nil
74
-			}
75
-		}
76
-		if err = json.Unmarshal(body, &data); err != nil {
77
-			glog.V(4).Infof("Error unmarshalling payload %v, but continuing with build", err)
78
-			return nil, envvars, true, nil
79
-		}
80
-		if len(data.Env) > 0 && trigger.AllowEnv {
81
-			envvars = data.Env
82
-		}
83
-		if data.Git == nil {
84
-			glog.V(4).Infof("No git information for the generic webhook found in %s/%s", buildCfg.Namespace, buildCfg.Name)
85
-			return nil, envvars, true, nil
62
+	body, err := ioutil.ReadAll(req.Body)
63
+	if err != nil {
64
+		return revision, envvars, false, err
65
+	}
66
+
67
+	if len(body) == 0 {
68
+		return revision, envvars, true, nil
69
+	}
70
+
71
+	var data api.GenericWebHookEvent
72
+	if contentType == "application/yaml" {
73
+		body, err = yaml.ToJSON(body)
74
+		if err != nil {
75
+			warning := webhook.NewWarning(fmt.Sprintf("error converting payload to json: %v, ignoring payload and continuing with build", err))
76
+			return revision, envvars, true, warning
86 77
 		}
78
+	}
79
+	if err = json.Unmarshal(body, &data); err != nil {
80
+		warning := webhook.NewWarning(fmt.Sprintf("error unmarshalling payload: %v, ignoring payload and continuing with build", err))
81
+		return revision, envvars, true, warning
82
+	}
83
+	if len(data.Env) > 0 && trigger.AllowEnv {
84
+		envvars = data.Env
85
+	}
86
+	if buildCfg.Spec.Source.Git == nil {
87
+		// everything below here is specific to git-based builds
88
+		return revision, envvars, true, nil
89
+	}
90
+	if data.Git == nil {
91
+		warning := webhook.NewWarning("no git information found in payload, ignoring and continuing with build")
92
+		return revision, envvars, true, warning
93
+	}
87 94
 
88
-		if data.Git.Refs != nil {
89
-			for _, ref := range data.Git.Refs {
90
-				if webhook.GitRefMatches(ref.Ref, webhook.DefaultConfigRef, &buildCfg.Spec.Source) {
91
-					revision = &api.SourceRevision{
92
-						Git: &ref.GitSourceRevision,
93
-					}
94
-					return revision, envvars, true, nil
95
+	if data.Git.Refs != nil {
96
+		for _, ref := range data.Git.Refs {
97
+			if webhook.GitRefMatches(ref.Ref, webhook.DefaultConfigRef, &buildCfg.Spec.Source) {
98
+				revision = &api.SourceRevision{
99
+					Git: &ref.GitSourceRevision,
95 100
 				}
101
+				return revision, envvars, true, nil
96 102
 			}
97
-			glog.V(2).Infof("Skipping build for BuildConfig %s/%s. None of the supplied refs matched %q", buildCfg.Namespace, buildCfg, buildCfg.Spec.Source.Git.Ref)
98
-			return nil, envvars, false, nil
99
-		}
100
-		if !webhook.GitRefMatches(data.Git.Ref, webhook.DefaultConfigRef, &buildCfg.Spec.Source) {
101
-			glog.V(2).Infof("Skipping build for BuildConfig %s/%s. Branch reference from %q does not match configuration", buildCfg.Namespace, buildCfg.Name, data.Git.Ref)
102
-			return nil, envvars, false, nil
103
-		}
104
-		revision = &api.SourceRevision{
105
-			Git: &data.Git.GitSourceRevision,
106 103
 		}
104
+		warning := webhook.NewWarning(fmt.Sprintf("skipping build. None of the supplied refs matched %q", buildCfg.Spec.Source.Git.Ref))
105
+		return revision, envvars, false, warning
106
+	}
107
+	if !webhook.GitRefMatches(data.Git.Ref, webhook.DefaultConfigRef, &buildCfg.Spec.Source) {
108
+		warning := webhook.NewWarning(fmt.Sprintf("skipping build. Branch reference from %q does not match configuration", data.Git.Ref))
109
+		return revision, envvars, false, warning
110
+	}
111
+	revision = &api.SourceRevision{
112
+		Git: &data.Git.GitSourceRevision,
107 113
 	}
108 114
 	return revision, envvars, true, nil
109 115
 }
... ...
@@ -11,6 +11,8 @@ import (
11 11
 	"github.com/openshift/origin/pkg/build/api"
12 12
 	"github.com/openshift/origin/pkg/build/webhook"
13 13
 	kapi "k8s.io/kubernetes/pkg/api"
14
+	"k8s.io/kubernetes/pkg/api/errors"
15
+	"k8s.io/kubernetes/pkg/api/unversioned"
14 16
 )
15 17
 
16 18
 var mockBuildStrategy = api.BuildStrategy{
... ...
@@ -52,6 +54,24 @@ func GivenRequestWithRefsPayload(t *testing.T) *http.Request {
52 52
 	return req
53 53
 }
54 54
 
55
+func matchWarning(t *testing.T, err error, message string) {
56
+	status, ok := err.(*errors.StatusError)
57
+	if !ok {
58
+		t.Errorf("Expected %v to be a StatusError object", err)
59
+		return
60
+	}
61
+
62
+	if status.ErrStatus.Status != unversioned.StatusSuccess {
63
+		t.Errorf("Unexpected response status %v, expected %v", status.ErrStatus.Status, unversioned.StatusSuccess)
64
+	}
65
+	if status.ErrStatus.Code != http.StatusOK {
66
+		t.Errorf("Unexpected response code %v, expected %v", status.ErrStatus.Code, http.StatusOK)
67
+	}
68
+	if status.ErrStatus.Message != message {
69
+		t.Errorf("Unexpected response message %v, expected %v", status.ErrStatus.Message, message)
70
+	}
71
+}
72
+
55 73
 func TestVerifyRequestForMethod(t *testing.T) {
56 74
 	req := GivenRequest("GET")
57 75
 	buildConfig := &api.BuildConfig{
... ...
@@ -327,9 +347,7 @@ func TestExtractWithUnmatchedRefGitPayload(t *testing.T) {
327 327
 	plugin := New()
328 328
 	build, _, proceed, err := plugin.Extract(buildConfig, "secret100", "", req)
329 329
 
330
-	if err != nil {
331
-		t.Errorf("Unexpected error when triggering build: %v", err)
332
-	}
330
+	matchWarning(t, err, `skipping build. Branch reference from "refs/heads/master" does not match configuration`)
333 331
 	if proceed {
334 332
 		t.Error("Expected 'proceed' return value to be 'false' for unmatched refs")
335 333
 	}
... ...
@@ -471,9 +489,7 @@ func TestExtractWithUnmatchedGitRefsPayload(t *testing.T) {
471 471
 	plugin := New()
472 472
 	revision, _, proceed, err := plugin.Extract(buildConfig, "secret100", "", req)
473 473
 
474
-	if err != nil {
475
-		t.Errorf("Expected to be able to trigger a build without a payload error: %v", err)
476
-	}
474
+	matchWarning(t, err, `skipping build. None of the supplied refs matched "other"`)
477 475
 	if proceed {
478 476
 		t.Error("Expected 'proceed' return value to be 'false'")
479 477
 	}
... ...
@@ -627,9 +643,7 @@ func TestGitlabPush(t *testing.T) {
627 627
 	plugin := New()
628 628
 	_, _, proceed, err := plugin.Extract(buildConfig, "secret100", "", req)
629 629
 
630
-	if err != nil {
631
-		t.Errorf("Expected to be able to trigger a build without a payload error: %v", err)
632
-	}
630
+	matchWarning(t, err, "no git information found in payload, ignoring and continuing with build")
633 631
 	if !proceed {
634 632
 		t.Error("Expected 'proceed' return value to be 'true'")
635 633
 	}
... ...
@@ -699,9 +713,75 @@ func TestExtractWithUnmarshalError(t *testing.T) {
699 699
 	}
700 700
 	plugin := New()
701 701
 	revision, _, proceed, err := plugin.Extract(buildConfig, "secret100", "", req)
702
-	if err != nil {
703
-		t.Errorf("Expected to be able to trigger a build without a payload error: %v", err)
702
+	matchWarning(t, err, `error unmarshalling payload: invalid character '\x00' looking for beginning of value, ignoring payload and continuing with build`)
703
+	if !proceed {
704
+		t.Error("Expected 'proceed' return value to be 'true'")
705
+	}
706
+	if revision != nil {
707
+		t.Error("Expected the 'revision' return value to be nil")
708
+	}
709
+}
710
+
711
+func TestExtractWithUnmarshalErrorYAML(t *testing.T) {
712
+	req, _ := http.NewRequest("POST", "http://someurl.com", errJSON{})
713
+	req.Header.Add("Content-Type", "application/yaml")
714
+	buildConfig := &api.BuildConfig{
715
+		Spec: api.BuildConfigSpec{
716
+			Triggers: []api.BuildTriggerPolicy{
717
+				{
718
+					Type: api.GenericWebHookBuildTriggerType,
719
+					GenericWebHook: &api.WebHookTrigger{
720
+						Secret: "secret100",
721
+					},
722
+				},
723
+			},
724
+			CommonSpec: api.CommonSpec{
725
+				Source: api.BuildSource{
726
+					Git: &api.GitBuildSource{
727
+						Ref: "other",
728
+					},
729
+				},
730
+				Strategy: mockBuildStrategy,
731
+			},
732
+		},
733
+	}
734
+	plugin := New()
735
+	revision, _, proceed, err := plugin.Extract(buildConfig, "secret100", "", req)
736
+	matchWarning(t, err, "error converting payload to json: yaml: control characters are not allowed, ignoring payload and continuing with build")
737
+	if !proceed {
738
+		t.Error("Expected 'proceed' return value to be 'true'")
739
+	}
740
+	if revision != nil {
741
+		t.Error("Expected the 'revision' return value to be nil")
742
+	}
743
+}
744
+
745
+func TestExtractWithBadContentType(t *testing.T) {
746
+	req, _ := http.NewRequest("POST", "http://someurl.com", errJSON{})
747
+	req.Header.Add("Content-Type", "bad")
748
+	buildConfig := &api.BuildConfig{
749
+		Spec: api.BuildConfigSpec{
750
+			Triggers: []api.BuildTriggerPolicy{
751
+				{
752
+					Type: api.GenericWebHookBuildTriggerType,
753
+					GenericWebHook: &api.WebHookTrigger{
754
+						Secret: "secret100",
755
+					},
756
+				},
757
+			},
758
+			CommonSpec: api.CommonSpec{
759
+				Source: api.BuildSource{
760
+					Git: &api.GitBuildSource{
761
+						Ref: "other",
762
+					},
763
+				},
764
+				Strategy: mockBuildStrategy,
765
+			},
766
+		},
704 767
 	}
768
+	plugin := New()
769
+	revision, _, proceed, err := plugin.Extract(buildConfig, "secret100", "", req)
770
+	matchWarning(t, err, "invalid Content-Type on payload, ignoring payload and continuing with build")
705 771
 	if !proceed {
706 772
 		t.Error("Expected 'proceed' return value to be 'true'")
707 773
 	}
... ...
@@ -709,3 +789,39 @@ func TestExtractWithUnmarshalError(t *testing.T) {
709 709
 		t.Error("Expected the 'revision' return value to be nil")
710 710
 	}
711 711
 }
712
+
713
+func TestExtractWithUnparseableContentType(t *testing.T) {
714
+	req, _ := http.NewRequest("POST", "http://someurl.com", errJSON{})
715
+	req.Header.Add("Content-Type", "bad//bad")
716
+	buildConfig := &api.BuildConfig{
717
+		Spec: api.BuildConfigSpec{
718
+			Triggers: []api.BuildTriggerPolicy{
719
+				{
720
+					Type: api.GenericWebHookBuildTriggerType,
721
+					GenericWebHook: &api.WebHookTrigger{
722
+						Secret: "secret100",
723
+					},
724
+				},
725
+			},
726
+			CommonSpec: api.CommonSpec{
727
+				Source: api.BuildSource{
728
+					Git: &api.GitBuildSource{
729
+						Ref: "other",
730
+					},
731
+				},
732
+				Strategy: mockBuildStrategy,
733
+			},
734
+		},
735
+	}
736
+	plugin := New()
737
+	revision, _, proceed, err := plugin.Extract(buildConfig, "secret100", "", req)
738
+	if err == nil || err.Error() != "error parsing Content-Type: mime: expected token after slash" {
739
+		t.Errorf("Unexpected error %v", err)
740
+	}
741
+	if proceed {
742
+		t.Error("Expected 'proceed' return value to be 'false'")
743
+	}
744
+	if revision != nil {
745
+		t.Error("Expected the 'revision' return value to be nil")
746
+	}
747
+}
... ...
@@ -7,6 +7,8 @@ import (
7 7
 	"strings"
8 8
 
9 9
 	kapi "k8s.io/kubernetes/pkg/api"
10
+	kerrors "k8s.io/kubernetes/pkg/api/errors"
11
+	"k8s.io/kubernetes/pkg/api/unversioned"
10 12
 
11 13
 	buildapi "github.com/openshift/origin/pkg/build/api"
12 14
 )
... ...
@@ -76,3 +78,12 @@ func ValidateWebHookSecret(webHookTriggers []buildapi.BuildTriggerPolicy, secret
76 76
 	}
77 77
 	return nil, ErrSecretMismatch
78 78
 }
79
+
80
+// NewWarning returns an StatusError object with a http.StatusOK (200) code.
81
+func NewWarning(message string) *kerrors.StatusError {
82
+	return &kerrors.StatusError{ErrStatus: unversioned.Status{
83
+		Status:  unversioned.StatusSuccess,
84
+		Code:    http.StatusOK,
85
+		Message: message,
86
+	}}
87
+}