Skip to content

Commit

Permalink
Enable TriggerBindings to validate requests
Browse files Browse the repository at this point in the history
This PR resolves the issue tektoncd#45.
It assumes that a task has been defined which can validate requests.
That task will receive header and payload as params.
Before the creation of resources, task will be called alongwith serviceaccount
which has github-secret used to create webhook.
Assumption:
1. Task is defined in such a way that it can use headers and payload received as params.
2. Apart from serviceaccount, payload and headers, task doesn't need anything else.
3. Task gives us non zero exit if validation failed. A sample task and main.go is provided.
  • Loading branch information
khrm committed Sep 1, 2019
1 parent e80e8f3 commit 16c98b9
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 1 deletion.
1 change: 1 addition & 0 deletions cmd/eventlistenersink/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func main() {
DiscoveryClient: sinkClients.DiscoveryClient,
RESTClient: sinkClients.RESTClient,
TriggersClient: sinkClients.TriggersClient,
PipelineClient: sinkClients.PipelineClient,
EventListenerName: sinkArgs.ElName,
EventListenerNamespace: sinkArgs.ElNamespace,
}
Expand Down
40 changes: 40 additions & 0 deletions cmd/securetrigger/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"log"
"os"
)

func main() {

signature := os.Getenv("X-Hub-Signature")
if len(signature) == 0 {
log.Println("Err securing endpoint: no signature")
os.Exit(1)
}

secret := os.Getenv("Secret")
if len(secret) == 0 {
log.Println("Err securing endpoint: no secret")
os.Exit(1)
}

payload := os.Getenv("Payload")
if len(payload) == 0 {
log.Println("Err securing endpoint: no payload")
os.Exit(1)
}

mac := hmac.New(sha1.New, []byte(secret))
_, _ = mac.Write([]byte(payload))
expectedMAC := hex.EncodeToString(mac.Sum(nil))

if !hmac.Equal([]byte(signature[5:]), []byte(expectedMAC)) {
log.Println("Err securing endpoint: signature doesn't match")
os.Exit(1)
}

}
4 changes: 4 additions & 0 deletions docs/eventlisteners.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ spec:
name: pipeline-binding
template:
name: pipeline-template
validate:
taskref:
name: github-validate
serviceaccount: githubsa
```
27 changes: 27 additions & 0 deletions docs/validate-webhook.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
name: validate-webhook
spec:
inputs:
params:
- name: Payload
description: Payload of Event Received
- name: X-Hub-Signature
description: Hash of the Request Received
- name: Github-Secret
description: Secret used to configure webhook
steps:
- name: validate
image: quay.io/khrm/trigger-validate
command: ["/validate-webhook"]
env:
- name: Payload
value: $(inputs.params.Payload)
- name: X-Hub-Signature
value: $(inputs.params.X-Hub-Signature)
- name: Github-Secret
valueFrom:
secretKeyRef:
name: github
key: github-secret
3 changes: 3 additions & 0 deletions docs/validate-webook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Validate Webhook Tekton Task
The validate webhook task configures provides a task to validate an incoming event to the addressable endpoint.
Task receives request headers and payload as Params. Sample Task provided for github.
8 changes: 8 additions & 0 deletions pkg/apis/triggers/v1alpha1/event_listener_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1

import (
v1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"knative.dev/pkg/apis"
Expand All @@ -36,10 +37,17 @@ type EventListenerSpec struct {
//
// +k8s:deepcopy-gen=true
type Trigger struct {
TriggerValidate *TriggerValidate `json:"validate"`
TriggerBinding TriggerBindingRef `json:"binding"`
TriggerTemplate TriggerTemplateRef `json:"template"`
}

// TriggerValidate represents the image to run taskrun for validating that trigger comes from the source which is desired
type TriggerValidate struct {
TaskRef *v1alpha1.TaskRef `json:"taskRef"`
ServiceAccount string `json:"serviceAccount,omitempty"`
}

// TriggerBindingRef refers to a particular TriggerBinding resource.
type TriggerBindingRef struct {
Name string `json:"name"`
Expand Down
30 changes: 29 additions & 1 deletion pkg/apis/triggers/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions pkg/sink/initialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package sink

import (
"flag"

"golang.org/x/xerrors"

pipelineclientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned"
triggersclientset "github.com/tektoncd/triggers/pkg/client/clientset/versioned"
discoveryclient "k8s.io/client-go/discovery"
kubeclientset "k8s.io/client-go/kubernetes"
Expand Down Expand Up @@ -52,6 +54,7 @@ type SinkClients struct {
DiscoveryClient discoveryclient.DiscoveryInterface
RESTClient restclient.Interface
TriggersClient triggersclientset.Interface
PipelineClient pipelineclientset.Interface
}

// GetArgs returns the flagged SinkArgs
Expand Down Expand Up @@ -87,9 +90,15 @@ func ConfigureClients() (SinkClients, error) {
if err != nil {
return SinkClients{}, xerrors.Errorf("Failed to create TriggersClient: %s", err)
}
pipelineclient, err := pipelineclientset.NewForConfig(clusterConfig)
if err != nil {
return SinkClients{}, xerrors.Errorf("Failed to create PipelineClient: %s", err)
}

return SinkClients{
DiscoveryClient: kubeClient.Discovery(),
RESTClient: kubeClient.RESTClient(),
TriggersClient: triggersClient,
PipelineClient: pipelineclient,
}, nil
}
71 changes: 71 additions & 0 deletions pkg/sink/sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ package sink

import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"path"

pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
pipelineclientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned"
triggersv1 "github.com/tektoncd/triggers/pkg/apis/triggers/v1alpha1"
triggersclientset "github.com/tektoncd/triggers/pkg/client/clientset/versioned"

"github.com/tektoncd/triggers/pkg/template"
"github.com/tidwall/gjson"
"golang.org/x/xerrors"
Expand All @@ -36,6 +41,7 @@ type Resource struct {
TriggersClient triggersclientset.Interface
DiscoveryClient discoveryclient.DiscoveryInterface
RESTClient restclient.Interface
PipelineClient pipelineclientset.Interface
EventListenerName string
EventListenerNamespace string
}
Expand All @@ -55,6 +61,14 @@ func (r Resource) HandleEvent(response http.ResponseWriter, request *http.Reques

// Execute each Trigger
for _, trigger := range el.Spec.Triggers {
// Secure Endpoint
if trigger.TriggerValidate != nil {
if err := r.secureEndpoint(trigger, request.Header, event); err != nil {
log.Printf("Error securing Endpoint for TriggerBinding %s in Namespace %s: %s", trigger.TriggerBinding.Name, r.EventListenerNamespace, err)
continue
}
}

binding, err := template.ResolveBinding(trigger,
r.TriggersClient.TektonV1alpha1().TriggerBindings(r.EventListenerNamespace).Get,
r.TriggersClient.TektonV1alpha1().TriggerTemplates(r.EventListenerNamespace).Get)
Expand Down Expand Up @@ -136,3 +150,60 @@ func createRequestURI(apiVersion, namePlural, namespace string) string {
uri = path.Join(uri, namePlural)
return uri
}

func (r Resource) secureEndpoint(trigger triggersv1.Trigger, headers http.Header, payload []byte) error {

params := []pipelinev1.Param{}
params = append(params, pipelinev1.Param{
Name: "Payload",
Value: pipelinev1.ArrayOrString{
Type: pipelinev1.ParamTypeArray,
StringVal: string(payload),
},
})

for key := range headers {
params = append(params, pipelinev1.Param{
Name: key,
Value: pipelinev1.ArrayOrString{
Type: pipelinev1.ParamTypeArray,
StringVal: headers.Get(key),
},
})
}

tr := &pipelinev1.TaskRun{
ObjectMeta: metav1.ObjectMeta{
Namespace: r.EventListenerNamespace,
GenerateName: trigger.TriggerValidate.TaskRef.Name,
},
Spec: pipelinev1.TaskRunSpec{
Inputs: pipelinev1.TaskRunInputs{
Params: params,
},
TaskRef: trigger.TriggerValidate.TaskRef,
ServiceAccount: trigger.TriggerValidate.ServiceAccount,
},
}

tr, err := r.PipelineClient.TektonV1alpha1().TaskRuns(r.EventListenerNamespace).Create(tr)
if err != nil {
return err
}

for {
tr, err := r.PipelineClient.TektonV1alpha1().TaskRuns(r.EventListenerNamespace).Get(tr.Name, metav1.GetOptions{})
if err != nil {
return err
}

if tr.IsSuccessful() {
break
}

if tr.IsDone() && !tr.IsSuccessful() {
return errors.New("validation taskrun failed")
}
}
return nil
}

0 comments on commit 16c98b9

Please sign in to comment.