gitsources: create secret and webhook secret
Use the webhook secret on webhook creation and check it and webhook receive
This commit is contained in:
parent
2675aee333
commit
649c42f75b
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue