gitsources: create secret and webhook secret

Use the webhook secret on webhook creation and check it and webhook receive
This commit is contained in:
Simone Gotti 2019-05-07 18:29:31 +02:00
parent 2675aee333
commit 649c42f75b
10 changed files with 91 additions and 76 deletions

View File

@ -28,7 +28,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
gitsource "github.com/sorintlab/agola/internal/gitsources" gitsource "github.com/sorintlab/agola/internal/gitsources"
"github.com/sorintlab/agola/internal/services/types"
) )
var jsonContent = http.Header{"content-type": []string{"application/json"}} var jsonContent = http.Header{"content-type": []string{"application/json"}}
@ -166,10 +165,6 @@ func (c *Client) CreateCommitStatus(repopath, commitSHA string, status gitsource
return nil return nil
} }
func (c *Client) ParseWebhook(r *http.Request) (*types.WebhookData, error) {
return parseWebhook(r)
}
func (c *Client) ListUserRepos() ([]*gitsource.RepoInfo, error) { func (c *Client) ListUserRepos() ([]*gitsource.RepoInfo, error) {
return nil, nil return nil, nil
} }

View File

@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"path" "path"
"strconv" "strconv"
@ -40,31 +41,25 @@ const (
prActionSync = "synchronized" prActionSync = "synchronized"
) )
func parseWebhook(r *http.Request) (*types.WebhookData, error) { func (c *Client) ParseWebhook(r *http.Request, secret string) (*types.WebhookData, error) {
data, err := ioutil.ReadAll(io.LimitReader(r.Body, 10*1024*1024))
if err != nil {
return nil, err
}
switch r.Header.Get(hookEvent) { switch r.Header.Get(hookEvent) {
case hookPush: case hookPush:
return parsePushHook(r.Body) return parsePushHook(data)
case hookPullRequest: case hookPullRequest:
return parsePullRequestHook(r.Body) return parsePullRequestHook(data)
default: default:
return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent)) return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent))
} }
} }
func parsePush(r io.Reader) (*pushHook, error) { func parsePushHook(data []byte) (*types.WebhookData, error) {
push := new(pushHook) push := new(pushHook)
err := json.NewDecoder(r).Decode(push) err := json.Unmarshal(data, push)
return push, err
}
func parsePullRequest(r io.Reader) (*pullRequestHook, error) {
pr := new(pullRequestHook)
err := json.NewDecoder(r).Decode(pr)
return pr, err
}
func parsePushHook(payload io.Reader) (*types.WebhookData, error) {
push, err := parsePush(payload)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -72,8 +67,9 @@ func parsePushHook(payload io.Reader) (*types.WebhookData, error) {
return webhookDataFromPush(push) return webhookDataFromPush(push)
} }
func parsePullRequestHook(payload io.Reader) (*types.WebhookData, error) { func parsePullRequestHook(data []byte) (*types.WebhookData, error) {
prhook, err := parsePullRequest(payload) prhook := new(pullRequestHook)
err := json.Unmarshal(data, prhook)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -15,9 +15,13 @@
package gitea package gitea
import ( import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"path" "path"
"strconv" "strconv"
@ -29,7 +33,8 @@ import (
) )
const ( const (
hookEvent = "X-Gitea-Event" hookEvent = "X-Gitea-Event"
signatureHeader = "X-Gitea-Signature"
hookPush = "push" hookPush = "push"
hookPullRequest = "pull_request" hookPullRequest = "pull_request"
@ -40,31 +45,40 @@ const (
prActionSync = "synchronized" prActionSync = "synchronized"
) )
func (c *Client) ParseWebhook(r *http.Request) (*types.WebhookData, error) { func (c *Client) ParseWebhook(r *http.Request, secret string) (*types.WebhookData, error) {
data, err := ioutil.ReadAll(io.LimitReader(r.Body, 10*1024*1024))
if err != nil {
return nil, err
}
// verify signature
if secret != "" {
signature := r.Header.Get(signatureHeader)
ds, err := hex.DecodeString(signature)
if err != nil {
return nil, errors.Errorf("wrong webhook signature")
}
h := hmac.New(sha256.New, []byte(secret))
h.Write(data)
cs := h.Sum(nil)
if !hmac.Equal(cs, ds) {
return nil, errors.Errorf("wrong webhook signature")
}
}
switch r.Header.Get(hookEvent) { switch r.Header.Get(hookEvent) {
case hookPush: case hookPush:
return parsePushHook(r.Body) return parsePushHook(data)
case hookPullRequest: case hookPullRequest:
return parsePullRequestHook(r.Body) return parsePullRequestHook(data)
default: default:
return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent)) return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent))
} }
} }
func parsePush(r io.Reader) (*pushHook, error) { func parsePushHook(data []byte) (*types.WebhookData, error) {
push := new(pushHook) push := new(pushHook)
err := json.NewDecoder(r).Decode(push) err := json.Unmarshal(data, push)
return push, err
}
func parsePullRequest(r io.Reader) (*pullRequestHook, error) {
pr := new(pullRequestHook)
err := json.NewDecoder(r).Decode(pr)
return pr, err
}
func parsePushHook(payload io.Reader) (*types.WebhookData, error) {
push, err := parsePush(payload)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -72,8 +86,9 @@ func parsePushHook(payload io.Reader) (*types.WebhookData, error) {
return webhookDataFromPush(push) return webhookDataFromPush(push)
} }
func parsePullRequestHook(payload io.Reader) (*types.WebhookData, error) { func parsePullRequestHook(data []byte) (*types.WebhookData, error) {
prhook, err := parsePullRequest(payload) prhook := new(pullRequestHook)
err := json.Unmarshal(data, prhook)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -219,6 +219,7 @@ func (c *Client) CreateRepoWebhook(repopath, url, secret string) error {
PushEvents: gitlab.Bool(true), PushEvents: gitlab.Bool(true),
TagPushEvents: gitlab.Bool(true), TagPushEvents: gitlab.Bool(true),
MergeRequestsEvents: gitlab.Bool(true), MergeRequestsEvents: gitlab.Bool(true),
Token: gitlab.String(secret),
} }
_, _, err := c.client.Projects.AddProjectHook(repopath, opts) _, _, err := c.client.Projects.AddProjectHook(repopath, opts)

View File

@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -28,7 +29,8 @@ import (
) )
const ( const (
hookEvent = "X-Gitlab-Event" hookEvent = "X-Gitlab-Event"
tokenHeader = "X-Gitlab-Token"
hookPush = "Push Hook" hookPush = "Push Hook"
hookTagPush = "Tag Push Hook" hookTagPush = "Tag Push Hook"
@ -40,33 +42,36 @@ const (
prActionSync = "synchronized" prActionSync = "synchronized"
) )
func (c *Client) ParseWebhook(r *http.Request) (*types.WebhookData, error) { func (c *Client) ParseWebhook(r *http.Request, secret string) (*types.WebhookData, error) {
data, err := ioutil.ReadAll(io.LimitReader(r.Body, 10*1024*1024))
if err != nil {
return nil, err
}
// verify token (gitlab doesn't sign the payload but just returns the provided
// secret)
if secret != "" {
token := r.Header.Get(tokenHeader)
if token != secret {
return nil, errors.Errorf("wrong webhook token")
}
}
switch r.Header.Get(hookEvent) { switch r.Header.Get(hookEvent) {
case hookPush: case hookPush:
return parsePushHook(r.Body) return parsePushHook(data)
case hookTagPush: case hookTagPush:
return parsePushHook(r.Body) return parsePushHook(data)
case hookPullRequest: case hookPullRequest:
return parsePullRequestHook(r.Body) return parsePullRequestHook(data)
default: default:
return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent)) return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent))
} }
} }
func parsePush(r io.Reader) (*pushHook, error) { func parsePushHook(data []byte) (*types.WebhookData, error) {
push := new(pushHook) push := new(pushHook)
err := json.NewDecoder(r).Decode(push) err := json.Unmarshal(data, push)
return push, err
}
func parsePullRequest(r io.Reader) (*pullRequestHook, error) {
pr := new(pullRequestHook)
err := json.NewDecoder(r).Decode(pr)
return pr, err
}
func parsePushHook(payload io.Reader) (*types.WebhookData, error) {
push, err := parsePush(payload)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -79,20 +84,15 @@ func parsePushHook(payload io.Reader) (*types.WebhookData, error) {
return webhookDataFromPush(push) return webhookDataFromPush(push)
} }
func parsePullRequestHook(payload io.Reader) (*types.WebhookData, error) { func parsePullRequestHook(data []byte) (*types.WebhookData, error) {
prhook, err := parsePullRequest(payload) prhook := new(pullRequestHook)
err := json.Unmarshal(data, prhook)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// // skip non open pull requests // TODO(sgotti) skip non open pull requests
// if prhook.PullRequest.State != prStateOpen { // TODO(sgotti) only accept actions that have new commits
// return nil, nil
// }
// // only accept actions that have new commits
// if prhook.Action != prActionOpen && prhook.Action != prActionSync {
// return nil, nil
// }
return webhookDataFromPullRequest(prhook), nil return webhookDataFromPullRequest(prhook), nil
} }

View File

@ -37,7 +37,7 @@ type GitSource interface {
UpdateDeployKey(repopath, title, pubKey string, readonly bool) error UpdateDeployKey(repopath, title, pubKey string, readonly bool) error
DeleteRepoWebhook(repopath, url string) error DeleteRepoWebhook(repopath, url string) error
CreateRepoWebhook(repopath, url, secret string) error CreateRepoWebhook(repopath, url, secret string) error
ParseWebhook(r *http.Request) (*types.WebhookData, error) ParseWebhook(r *http.Request, secret string) (*types.WebhookData, error)
CreateCommitStatus(repopath, commitSHA string, status CommitStatus, targetURL, description, context string) error CreateCommitStatus(repopath, commitSHA string, status CommitStatus, targetURL, description, context string) error
ListUserRepos() ([]*RepoInfo, error) ListUserRepos() ([]*RepoInfo, error)
} }

View File

@ -133,7 +133,9 @@ func (h *ActionHandler) CreateProject(ctx context.Context, project *types.Projec
project.ID = uuid.NewV4().String() project.ID = uuid.NewV4().String()
project.Parent.Type = types.ConfigTypeProjectGroup project.Parent.Type = types.ConfigTypeProjectGroup
// generate the Secret and the WebhookSecret
project.Secret = util.EncodeSha1Hex(uuid.NewV4().String()) project.Secret = util.EncodeSha1Hex(uuid.NewV4().String())
project.WebhookSecret = util.EncodeSha1Hex(uuid.NewV4().String())
pcj, err := json.Marshal(project) pcj, err := json.Marshal(project)
if err != nil { if err != nil {

View File

@ -194,7 +194,7 @@ func (h *ActionHandler) SetupProject(ctx context.Context, rs *types.RemoteSource
return errors.Wrapf(err, "failed to delete repository webhook") return errors.Wrapf(err, "failed to delete repository webhook")
} }
h.log.Infof("creating webhook to url: %s", webhookURL) h.log.Infof("creating webhook to url: %s", webhookURL)
if err := gitsource.CreateRepoWebhook(project.RepositoryPath, webhookURL.String(), ""); err != nil { if err := gitsource.CreateRepoWebhook(project.RepositoryPath, webhookURL.String(), project.WebhookSecret); err != nil {
return errors.Wrapf(err, "failed to create repository webhook") return errors.Wrapf(err, "failed to create repository webhook")
} }

View File

@ -155,7 +155,7 @@ func (h *webhooksHandler) handleWebhook(r *http.Request) (int, string, error) {
skipSSHHostKeyCheck = project.SkipSSHHostKeyCheck skipSSHHostKeyCheck = project.SkipSSHHostKeyCheck
} }
runType = types.RunTypeProject runType = types.RunTypeProject
webhookData, err = gitSource.ParseWebhook(r) webhookData, err = gitSource.ParseWebhook(r, project.WebhookSecret)
if err != nil { if err != nil {
return http.StatusBadRequest, "", errors.Wrapf(err, "failed to parse webhook") return http.StatusBadRequest, "", errors.Wrapf(err, "failed to parse webhook")
} }
@ -213,7 +213,7 @@ func (h *webhooksHandler) handleWebhook(r *http.Request) (int, string, error) {
} else { } else {
gitSource = agolagit.New(h.apiExposedURL + "/repos") gitSource = agolagit.New(h.apiExposedURL + "/repos")
var err error var err error
webhookData, err = gitSource.ParseWebhook(r) webhookData, err = gitSource.ParseWebhook(r, "")
if err != nil { if err != nil {
return http.StatusBadRequest, "", errors.Wrapf(err, "failed to parse webhook") return http.StatusBadRequest, "", errors.Wrapf(err, "failed to parse webhook")
} }

View File

@ -73,7 +73,8 @@ type User struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// A secret string that could be used for signing or other purposes // Secret is a secret that could be used for signing or other purposes. It
// should never be directly exposed to external services
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
LinkedAccounts map[string]*LinkedAccount `json:"linked_accounts,omitempty"` LinkedAccounts map[string]*LinkedAccount `json:"linked_accounts,omitempty"`
@ -236,7 +237,8 @@ type Project struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// A secret string that could be used for signing or other purposes // Secret is a secret that could be used for signing or other purposes. It
// should never be directly exposed to external services
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
Parent Parent `json:"parent,omitempty"` Parent Parent `json:"parent,omitempty"`
@ -263,6 +265,10 @@ type Project struct {
SSHPrivateKey string `json:"ssh_private_key,omitempty"` // PEM Encoded private key SSHPrivateKey string `json:"ssh_private_key,omitempty"` // PEM Encoded private key
SkipSSHHostKeyCheck bool `json:"skip_ssh_host_key_check,omitempty"` SkipSSHHostKeyCheck bool `json:"skip_ssh_host_key_check,omitempty"`
// Webhooksecret is the secret passed to git sources that support a
// secret/token for signing or verifying the webhook payload
WebhookSecret string `json:"webhook_secret,omitempty"`
} }
type SecretType string type SecretType string