added webhook validation
This commit is contained in:
parent
a021fab543
commit
dd88498e0d
6 changed files with 158 additions and 21 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
env.txt
|
||||
secret.yaml
|
||||
webhook-secret.yaml
|
||||
|
|
|
|||
27
README.md
27
README.md
|
|
@ -10,6 +10,8 @@ This way we can trigger cleanups when the PR is closed.
|
|||
|
||||
```
|
||||
X-Interceptor-Push-Ref: string: "refs/heads/master"
|
||||
X-Interceptor-Include-Repo: []string{"my/repo"}
|
||||
X-Interceptor-Exclude-Repo: []string{"my/repo"}
|
||||
```
|
||||
|
||||
Response Headers:
|
||||
|
|
@ -53,10 +55,33 @@ Issue: <Pull-Request ID>
|
|||
Commit: <Pull-Request Head Commit Hash>
|
||||
```
|
||||
|
||||
## Validate Webhooks
|
||||
|
||||
Add secret webhook keys you want to have validated.
|
||||
|
||||
**NOTE:** the name must be `webhook-secret`.
|
||||
|
||||
```
|
||||
cat <<EOF>webhook-secret.yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: webhook-secret
|
||||
namespace: webhook-interceptor
|
||||
type: Opaque
|
||||
stringData:
|
||||
github.com: "<secreet>"
|
||||
github.com_foo: "<foo secret>"
|
||||
github.com_foo_bar: "<bar secret>"
|
||||
EOF
|
||||
```
|
||||
|
||||
# Build and run
|
||||
|
||||
Create git authentication secret:
|
||||
|
||||
**NOTE:** the name must be `git-auth`.
|
||||
|
||||
```
|
||||
cat <<EOF>secret.yaml
|
||||
apiVersion: v1
|
||||
|
|
@ -67,6 +92,8 @@ metadata:
|
|||
type: Opaque
|
||||
stringData:
|
||||
github.com: "<username>:<password>"
|
||||
github.com_foo: "<foo username>:<foo password>"
|
||||
github.com_foo_bar: "<bar username>:<bar password>"
|
||||
EOF
|
||||
```
|
||||
|
||||
|
|
|
|||
1
build.sh
1
build.sh
|
|
@ -4,5 +4,6 @@ source env.txt
|
|||
kubectl -n webhook-interceptor delete deploy webhook-interceptor
|
||||
ko apply -f config/
|
||||
kubectl apply -f secret.yaml
|
||||
kubectl apply -f webhook-secret.yaml
|
||||
sleep 10
|
||||
kubectl -n webhook-interceptor logs -f $(kubectl -n webhook-interceptor get pod -ojsonpath='{.items[0].metadata.name}')
|
||||
|
|
|
|||
100
git.go
100
git.go
|
|
@ -4,9 +4,14 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
|
@ -19,7 +24,10 @@ const (
|
|||
contentType = "Content-Type"
|
||||
jsonType = "application/json"
|
||||
|
||||
secretName = "git-auth"
|
||||
authSecretName = "git-auth"
|
||||
validateSecretName = "webhook-secret"
|
||||
|
||||
secretDelim = "_"
|
||||
)
|
||||
|
||||
type Git struct {
|
||||
|
|
@ -27,17 +35,82 @@ type Git struct {
|
|||
cl *http.Client
|
||||
}
|
||||
|
||||
func (g *Git) getSecret(host string) (username, password string, err error) {
|
||||
sec, err := g.sec.Get(secretName, metav1.GetOptions{})
|
||||
func makeKeys(host, owner, repo string) []string {
|
||||
return []string{
|
||||
host + secretDelim + owner + secretDelim + repo,
|
||||
host + secretDelim + owner,
|
||||
host,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Git) getValidateSecret(host, owner, repo string) (key []byte, ok bool) {
|
||||
sec, err := g.sec.Get(validateSecretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s, ok := sec.Data[host]
|
||||
for _, v := range makeKeys(host, owner, repo) {
|
||||
key, ok = sec.Data[v]
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *Git) validate(r *http.Request, payload []byte, repo Repository) (valid bool) {
|
||||
var (
|
||||
h hash.Hash
|
||||
sig []byte
|
||||
err error
|
||||
)
|
||||
u, err := url.Parse(repo.CloneURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key, ok := g.getValidateSecret(u.Host, repo.Owner.Login, repo.Name)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
api := detectAPI(r)
|
||||
switch api {
|
||||
case giteaAPI:
|
||||
sig, err = hex.DecodeString(r.Header.Get("X-Gitea-Signature"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h = hmac.New(sha256.New, key)
|
||||
case githubAPI:
|
||||
sig, err = hex.DecodeString(strings.TrimPrefix(r.Header.Get("X-Hub-Signature"), "sha1="))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h = hmac.New(sha1.New, key)
|
||||
}
|
||||
h.Write(payload)
|
||||
return hmac.Equal(h.Sum(nil), sig)
|
||||
|
||||
}
|
||||
|
||||
func (g *Git) getAuthSecret(host, owner, repo string) (username, password string, err error) {
|
||||
sec, err := g.sec.Get(authSecretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var (
|
||||
secret []byte
|
||||
ok bool
|
||||
)
|
||||
for _, v := range makeKeys(host, owner, repo) {
|
||||
secret, ok = sec.Data[v]
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
err = fmt.Errorf("no key found for host %s in secret %s/%s", host, sec.Namespace, sec.Name)
|
||||
return
|
||||
}
|
||||
a := strings.SplitN(string(s), ":", 2)
|
||||
a := strings.SplitN(string(secret), ":", 2)
|
||||
if len(a) < 2 {
|
||||
err = fmt.Errorf("missing credentials for host %s in secret %s/%s", host, sec.Namespace, sec.Name)
|
||||
return
|
||||
|
|
@ -47,8 +120,8 @@ func (g *Git) getSecret(host string) (username, password string, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (g *Git) send(req *http.Request) (*http.Response, error) {
|
||||
user, pass, err := g.getSecret(req.URL.Host)
|
||||
func (g *Git) send(req *http.Request, owner, repo string) (*http.Response, error) {
|
||||
user, pass, err := g.getAuthSecret(req.URL.Host, owner, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -69,7 +142,7 @@ func (g *Git) getPullRequest(repo Repository, api, id string) (pr PullRequest, e
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := g.send(req)
|
||||
resp, err := g.send(req, repo.Owner.Login, repo.Name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -97,7 +170,7 @@ func (g *Git) addLabel(repo Repository, api, label, color string) (labelID int64
|
|||
return
|
||||
}
|
||||
|
||||
resp, err := g.send(req)
|
||||
resp, err := g.send(req, repo.Owner.Login, repo.Name)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error getting labels of %s: %s", u, err)
|
||||
return
|
||||
|
|
@ -121,6 +194,9 @@ func (g *Git) addLabel(repo Repository, api, label, color string) (labelID int64
|
|||
return
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if api == githubAPI {
|
||||
color = strings.TrimPrefix(color, "#")
|
||||
}
|
||||
l := &Label{Name: label, Color: color}
|
||||
err = json.NewEncoder(buf).Encode(l)
|
||||
if err != nil {
|
||||
|
|
@ -131,7 +207,7 @@ func (g *Git) addLabel(repo Repository, api, label, color string) (labelID int64
|
|||
return
|
||||
}
|
||||
|
||||
resp, err = g.send(req)
|
||||
resp, err = g.send(req, repo.Owner.Login, repo.Name)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error creating label %s for %s: %s", label, u, err)
|
||||
return
|
||||
|
|
@ -162,7 +238,7 @@ func (g *Git) setLabel(repo Repository, api, id, label, color string, remove []s
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := g.send(req)
|
||||
resp, err := g.send(req, repo.Owner.Login, repo.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting issue labels of %s: %s", u, err)
|
||||
}
|
||||
|
|
@ -207,7 +283,7 @@ func (g *Git) setLabel(repo Repository, api, id, label, color string, remove []s
|
|||
return
|
||||
}
|
||||
|
||||
resp, err = g.send(req)
|
||||
resp, err = g.send(req, repo.Owner.Login, repo.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting issue label %s for %s: %s", label, u, err)
|
||||
}
|
||||
|
|
|
|||
43
main.go
43
main.go
|
|
@ -25,6 +25,8 @@ const (
|
|||
commentAction = "X-Interceptor-Comment-Action"
|
||||
labelHeader = "X-Interceptor-Label"
|
||||
removeLabelsHeader = "X-Interceptor-Remove-Labels"
|
||||
includeHeader = "X-Interceptor-Include-Repo"
|
||||
excludeHeader = "X-Interceptor-Exclude-Repo"
|
||||
|
||||
githubEvent = "X-GitHub-Event"
|
||||
giteaEvent = "X-Gitea-Event"
|
||||
|
|
@ -42,28 +44,33 @@ type Handler struct {
|
|||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var valid bool
|
||||
event := r.Header.Get(githubEvent)
|
||||
switch event {
|
||||
case "pull_request":
|
||||
h.prHandler(w, r)
|
||||
valid = h.prHandler(w, r)
|
||||
case "push":
|
||||
h.pushHandler(w, r)
|
||||
valid = h.pushHandler(w, r)
|
||||
case "issue_comment":
|
||||
h.commentHandler(w, r)
|
||||
valid = h.commentHandler(w, r)
|
||||
default:
|
||||
h.dumpHandler(w, r)
|
||||
}
|
||||
status := int(reflect.Indirect(reflect.ValueOf(w)).FieldByName("status").Int())
|
||||
log.Printf("%s %s %s event:%s status:%d", r.RemoteAddr, r.Method, r.URL, event, status)
|
||||
log.Printf("%s %s %s event:%s valid:%t status:%d", r.RemoteAddr, r.Method, r.URL, event, valid, status)
|
||||
}
|
||||
|
||||
func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) (valid bool) {
|
||||
var prb PullRequestBody
|
||||
body, err := readBody(r.Body, &prb)
|
||||
if err != nil {
|
||||
logError(w, err.Error())
|
||||
return
|
||||
}
|
||||
if valid = h.git.validate(r, body, prb.Repository); !valid {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if contains(r.Header[prAction], prb.Action) {
|
||||
if l := r.Header.Get(labelHeader); l != "" {
|
||||
|
|
@ -85,33 +92,54 @@ func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) pushHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) pushHandler(w http.ResponseWriter, r *http.Request) (valid bool) {
|
||||
var pb PushBody
|
||||
body, err := readBody(r.Body, &pb)
|
||||
if err != nil {
|
||||
logError(w, err.Error())
|
||||
return
|
||||
}
|
||||
if valid = h.git.validate(r, body, pb.Repository); !valid {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
push := r.Header.Get(pushRef)
|
||||
//if contains(r.Header[pushAction], pb.Ref) {
|
||||
if push != "" && push == pb.Ref {
|
||||
inc := r.Header[includeHeader]
|
||||
exc := r.Header[excludeHeader]
|
||||
if inc != nil && !contains(inc, pb.Repository.FullName) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if exc != nil && contains(exc, pb.Repository.FullName) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h := w.Header()
|
||||
h.Set(commitHeader, pb.After)
|
||||
w.Write(body)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) commentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) commentHandler(w http.ResponseWriter, r *http.Request) (valid bool) {
|
||||
var cb CommentBody
|
||||
body, err := readBody(r.Body, &cb)
|
||||
if err != nil {
|
||||
logError(w, err.Error())
|
||||
return
|
||||
}
|
||||
if valid = h.git.validate(r, body, cb.Repository); !valid {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !contains(r.Header[commentAction], cb.Action) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
|
@ -163,6 +191,7 @@ func (h *Handler) commentHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) dumpHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
7
types.go
7
types.go
|
|
@ -6,6 +6,7 @@ type PullRequestBody struct {
|
|||
Action string `json:"action"`
|
||||
Number int64 `json:"number"`
|
||||
PullRequest PullRequest `json:"pull_request"`
|
||||
Repository Repository `json:"repository"`
|
||||
}
|
||||
|
||||
type PullRequest struct {
|
||||
|
|
@ -32,8 +33,9 @@ type PostLabelsInt struct {
|
|||
}
|
||||
|
||||
type PushBody struct {
|
||||
Ref string `json:"ref"`
|
||||
After string `json:"after"`
|
||||
Ref string `json:"ref"`
|
||||
After string `json:"after"`
|
||||
Repository Repository `json:"repository"`
|
||||
}
|
||||
|
||||
type CommentBody struct {
|
||||
|
|
@ -54,6 +56,7 @@ type Comment struct {
|
|||
|
||||
type Repository struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Owner Owner `json:"owner"`
|
||||
CloneURL string `json:"clone_url"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue