diff --git a/app/api/controller/webhook/preprocessor.go b/app/api/controller/webhook/preprocessor.go index 534f00239..462610018 100644 --- a/app/api/controller/webhook/preprocessor.go +++ b/app/api/controller/webhook/preprocessor.go @@ -20,8 +20,16 @@ import ( ) type Preprocessor interface { - PreprocessCreateInput(enum.PrincipalType, *types.WebhookCreateInput) (enum.WebhookType, error) - PreprocessUpdateInput(enum.PrincipalType, *types.WebhookUpdateInput) (enum.WebhookType, error) + PreprocessCreateInput( + enum.PrincipalType, + *types.WebhookCreateInput, + *types.WebhookSignatureMetadata, + ) (enum.WebhookType, error) + PreprocessUpdateInput( + enum.PrincipalType, + *types.WebhookUpdateInput, + *types.WebhookSignatureMetadata, + ) (enum.WebhookType, error) PreprocessFilter(enum.PrincipalType, *types.WebhookFilter) IsInternalCall(enum.PrincipalType) bool } @@ -33,6 +41,7 @@ type NoopPreprocessor struct { func (p NoopPreprocessor) PreprocessCreateInput( enum.PrincipalType, *types.WebhookCreateInput, + *types.WebhookSignatureMetadata, ) (enum.WebhookType, error) { return enum.WebhookTypeExternal, nil } @@ -41,6 +50,7 @@ func (p NoopPreprocessor) PreprocessCreateInput( func (p NoopPreprocessor) PreprocessUpdateInput( enum.PrincipalType, *types.WebhookUpdateInput, + *types.WebhookSignatureMetadata, ) (enum.WebhookType, error) { return enum.WebhookTypeExternal, nil } diff --git a/app/api/controller/webhook/repo_create.go b/app/api/controller/webhook/repo_create.go index 1631dcbd5..4feb97497 100644 --- a/app/api/controller/webhook/repo_create.go +++ b/app/api/controller/webhook/repo_create.go @@ -29,13 +29,14 @@ func (c *Controller) CreateRepo( session *auth.Session, repoRef string, in *types.WebhookCreateInput, + signatureData *types.WebhookSignatureMetadata, ) (*types.Webhook, error) { repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) if err != nil { return nil, fmt.Errorf("failed to acquire access to the repo: %w", err) } - typ, err := c.preprocessor.PreprocessCreateInput(session.Principal.Type, in) + typ, err := c.preprocessor.PreprocessCreateInput(session.Principal.Type, in, signatureData) if err != nil { return nil, fmt.Errorf("failed to preprocess create input: %w", err) } diff --git a/app/api/controller/webhook/repo_update.go b/app/api/controller/webhook/repo_update.go index c7f3fdd9f..e5759f29e 100644 --- a/app/api/controller/webhook/repo_update.go +++ b/app/api/controller/webhook/repo_update.go @@ -30,18 +30,17 @@ func (c *Controller) UpdateRepo( repoRef string, webhookIdentifier string, in *types.WebhookUpdateInput, + signatureData *types.WebhookSignatureMetadata, ) (*types.Webhook, error) { repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) if err != nil { return nil, fmt.Errorf("failed to acquire access to the repo: %w", err) } - typ, err := c.preprocessor.PreprocessUpdateInput(session.Principal.Type, in) + typ, err := c.preprocessor.PreprocessUpdateInput(session.Principal.Type, in, signatureData) if err != nil { return nil, fmt.Errorf("failed to preprocess update input: %w", err) } - return c.webhookService.Update( - ctx, repo.ID, enum.WebhookParentRepo, webhookIdentifier, typ, in, - ) + return c.webhookService.Update(ctx, repo.ID, enum.WebhookParentRepo, webhookIdentifier, typ, in) } diff --git a/app/api/controller/webhook/space_create.go b/app/api/controller/webhook/space_create.go index b77b488b8..513b2436d 100644 --- a/app/api/controller/webhook/space_create.go +++ b/app/api/controller/webhook/space_create.go @@ -29,20 +29,19 @@ func (c *Controller) CreateSpace( session *auth.Session, spaceRef string, in *types.WebhookCreateInput, + signatureData *types.WebhookSignatureMetadata, ) (*types.Webhook, error) { space, err := c.getSpaceCheckAccess(ctx, session, spaceRef, enum.PermissionSpaceEdit) if err != nil { return nil, fmt.Errorf("failed to acquire access to space: %w", err) } - internal, err := c.preprocessor.PreprocessCreateInput(session.Principal.Type, in) + internal, err := c.preprocessor.PreprocessCreateInput(session.Principal.Type, in, signatureData) if err != nil { return nil, fmt.Errorf("failed to preprocess create input: %w", err) } - hook, err := c.webhookService.Create( - ctx, session.Principal.ID, space.ID, enum.WebhookParentSpace, internal, in, - ) + hook, err := c.webhookService.Create(ctx, session.Principal.ID, space.ID, enum.WebhookParentSpace, internal, in) if err != nil { return nil, fmt.Errorf("failed to create webhook: %w", err) } diff --git a/app/api/controller/webhook/space_update.go b/app/api/controller/webhook/space_update.go index 0af3ba733..c85872ac1 100644 --- a/app/api/controller/webhook/space_update.go +++ b/app/api/controller/webhook/space_update.go @@ -30,13 +30,14 @@ func (c *Controller) UpdateSpace( spaceRef string, webhookIdentifier string, in *types.WebhookUpdateInput, + signatureData *types.WebhookSignatureMetadata, ) (*types.Webhook, error) { space, err := c.getSpaceCheckAccess(ctx, session, spaceRef, enum.PermissionSpaceEdit) if err != nil { return nil, fmt.Errorf("failed to acquire access to space: %w", err) } - typ, err := c.preprocessor.PreprocessUpdateInput(session.Principal.Type, in) + typ, err := c.preprocessor.PreprocessUpdateInput(session.Principal.Type, in, signatureData) if err != nil { return nil, fmt.Errorf("failed to preprocess update input: %w", err) } diff --git a/app/api/handler/webhook/repo_create.go b/app/api/handler/webhook/repo_create.go index 62aeca379..873d4f9c0 100644 --- a/app/api/handler/webhook/repo_create.go +++ b/app/api/handler/webhook/repo_create.go @@ -15,7 +15,9 @@ package webhook import ( + "bytes" "encoding/json" + "io" "net/http" "github.com/harness/gitness/app/api/controller/webhook" @@ -29,6 +31,11 @@ func HandleCreateRepo(webhookCtrl *webhook.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) + return + } repoRef, err := request.GetRepoRefFromPath(r) if err != nil { @@ -37,13 +44,22 @@ func HandleCreateRepo(webhookCtrl *webhook.Controller) http.HandlerFunc { } in := new(types.WebhookCreateInput) - err = json.NewDecoder(r.Body).Decode(in) + readerCloser := io.NopCloser(bytes.NewReader(bodyBytes)) + err = json.NewDecoder(readerCloser).Decode(in) if err != nil { render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) return } - hook, err := webhookCtrl.CreateRepo(ctx, session, repoRef, in) + var signatureData *types.WebhookSignatureMetadata + signature := request.GetSignatureFromHeaderOrDefault(r, "") + if signature != "" { + signatureData = new(types.WebhookSignatureMetadata) + signatureData.Signature = signature + signatureData.BodyBytes = bodyBytes + } + + hook, err := webhookCtrl.CreateRepo(ctx, session, repoRef, in, signatureData) if err != nil { render.TranslatedUserError(ctx, w, err) return diff --git a/app/api/handler/webhook/repo_update.go b/app/api/handler/webhook/repo_update.go index 5428411b7..23f8b06af 100644 --- a/app/api/handler/webhook/repo_update.go +++ b/app/api/handler/webhook/repo_update.go @@ -15,7 +15,9 @@ package webhook import ( + "bytes" "encoding/json" + "io" "net/http" "github.com/harness/gitness/app/api/controller/webhook" @@ -29,6 +31,11 @@ func HandleUpdateRepo(webhookCtrl *webhook.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) + return + } repoRef, err := request.GetRepoRefFromPath(r) if err != nil { @@ -43,13 +50,22 @@ func HandleUpdateRepo(webhookCtrl *webhook.Controller) http.HandlerFunc { } in := new(types.WebhookUpdateInput) - err = json.NewDecoder(r.Body).Decode(in) + readerCloser := io.NopCloser(bytes.NewReader(bodyBytes)) + err = json.NewDecoder(readerCloser).Decode(in) if err != nil { render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) return } - hook, err := webhookCtrl.UpdateRepo(ctx, session, repoRef, webhookIdentifier, in) + var signatureData *types.WebhookSignatureMetadata + signature := request.GetSignatureFromHeaderOrDefault(r, "") + if signature != "" { + signatureData = new(types.WebhookSignatureMetadata) + signatureData.Signature = signature + signatureData.BodyBytes = bodyBytes + } + + hook, err := webhookCtrl.UpdateRepo(ctx, session, repoRef, webhookIdentifier, in, signatureData) if err != nil { render.TranslatedUserError(ctx, w, err) return diff --git a/app/api/handler/webhook/space_create.go b/app/api/handler/webhook/space_create.go index 4a8050c3c..b90b6b4cb 100644 --- a/app/api/handler/webhook/space_create.go +++ b/app/api/handler/webhook/space_create.go @@ -15,7 +15,9 @@ package webhook import ( + "bytes" "encoding/json" + "io" "net/http" "github.com/harness/gitness/app/api/controller/webhook" @@ -29,6 +31,11 @@ func HandleCreateSpace(webhookCtrl *webhook.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) + return + } spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { @@ -37,13 +44,22 @@ func HandleCreateSpace(webhookCtrl *webhook.Controller) http.HandlerFunc { } in := new(types.WebhookCreateInput) - err = json.NewDecoder(r.Body).Decode(in) + readerCloser := io.NopCloser(bytes.NewReader(bodyBytes)) + err = json.NewDecoder(readerCloser).Decode(in) if err != nil { render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) return } - hook, err := webhookCtrl.CreateSpace(ctx, session, spaceRef, in) + var signatureData *types.WebhookSignatureMetadata + signature := request.GetSignatureFromHeaderOrDefault(r, "") + if signature != "" { + signatureData = new(types.WebhookSignatureMetadata) + signatureData.Signature = signature + signatureData.BodyBytes = bodyBytes + } + + hook, err := webhookCtrl.CreateSpace(ctx, session, spaceRef, in, signatureData) if err != nil { render.TranslatedUserError(ctx, w, err) return diff --git a/app/api/handler/webhook/space_update.go b/app/api/handler/webhook/space_update.go index 3b463ce5e..44cf1c16a 100644 --- a/app/api/handler/webhook/space_update.go +++ b/app/api/handler/webhook/space_update.go @@ -15,7 +15,9 @@ package webhook import ( + "bytes" "encoding/json" + "io" "net/http" "github.com/harness/gitness/app/api/controller/webhook" @@ -29,6 +31,11 @@ func HandleUpdateSpace(webhookCtrl *webhook.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session, _ := request.AuthSessionFrom(ctx) + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) + return + } spaceRef, err := request.GetSpaceRefFromPath(r) if err != nil { @@ -43,13 +50,22 @@ func HandleUpdateSpace(webhookCtrl *webhook.Controller) http.HandlerFunc { } in := new(types.WebhookUpdateInput) - err = json.NewDecoder(r.Body).Decode(in) + readerCloser := io.NopCloser(bytes.NewReader(bodyBytes)) + err = json.NewDecoder(readerCloser).Decode(in) if err != nil { render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) return } - hook, err := webhookCtrl.UpdateSpace(ctx, session, spaceRef, webhookIdentifier, in) + var signatureData *types.WebhookSignatureMetadata + signature := request.GetSignatureFromHeaderOrDefault(r, "") + if signature != "" { + signatureData = new(types.WebhookSignatureMetadata) + signatureData.Signature = signature + signatureData.BodyBytes = bodyBytes + } + + hook, err := webhookCtrl.UpdateSpace(ctx, session, spaceRef, webhookIdentifier, in, signatureData) if err != nil { render.TranslatedUserError(ctx, w, err) return diff --git a/app/api/request/common.go b/app/api/request/common.go index cc2343cb0..a11c51c64 100644 --- a/app/api/request/common.go +++ b/app/api/request/common.go @@ -67,6 +67,8 @@ const ( HeaderIfNoneMatch = "If-None-Match" HeaderETag = "ETag" + + HeaderSignature = "Signature" ) // GetOptionalRemainderFromPath returns the remainder ("*") from the path or an empty string if it doesn't exist. @@ -239,3 +241,7 @@ func GetDeletedAtFromQuery(r *http.Request) (int64, bool, error) { func GetIfNoneMatchFromHeader(r *http.Request) (string, bool) { return GetHeader(r, HeaderIfNoneMatch) } + +func GetSignatureFromHeaderOrDefault(r *http.Request, dflt string) string { + return GetHeaderOrDefault(r, HeaderSignature, dflt) +} diff --git a/app/services/webhook/trigger.go b/app/services/webhook/trigger.go index efdc69ea1..a4a1fcb47 100644 --- a/app/services/webhook/trigger.go +++ b/app/services/webhook/trigger.go @@ -17,9 +17,6 @@ package webhook import ( "bytes" "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -28,6 +25,7 @@ import ( "net/http" "time" + "github.com/harness/gitness/crypto" "github.com/harness/gitness/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" @@ -354,7 +352,7 @@ func (s *Service) prepareHTTPRequest(ctx context.Context, execution *types.Webho return nil, fmt.Errorf("failed to decrypt webhook secret: %w", err) } var hmac string - hmac, err = generateHMACSHA256(bBuff.Bytes(), []byte(decryptedSecret)) + hmac, err = crypto.GenerateHMACSHA256(bBuff.Bytes(), []byte(decryptedSecret)) if err != nil { return nil, fmt.Errorf("failed to generate SHA256 based HMAC: %w", err) } @@ -475,20 +473,3 @@ func handleWebhookResponse(execution *types.WebhookExecution, resp *http.Respons return fmt.Errorf("received response with unsupported status code %d", code) } } - -// generateHMACSHA256 generates a new HMAC using SHA256 as hash function. -func generateHMACSHA256(data []byte, key []byte) (string, error) { - h := hmac.New(sha256.New, key) - - // write all data into hash - _, err := h.Write(data) - if err != nil { - return "", fmt.Errorf("failed to write data into hash: %w", err) - } - - // sum hash to final value - macBytes := h.Sum(nil) - - // encode MAC as hexadecimal - return hex.EncodeToString(macBytes), nil -} diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 000000000..ca7d06da9 --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,43 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" +) + +// GenerateHMACSHA256 generates a new HMAC using SHA256 as hash function. +func GenerateHMACSHA256(data []byte, key []byte) (string, error) { + h := hmac.New(sha256.New, key) + + // write all data into hash + _, err := h.Write(data) + if err != nil { + return "", fmt.Errorf("failed to write data into hash: %w", err) + } + + // sum hash to final value + macBytes := h.Sum(nil) + + // encode MAC as hexadecimal + return hex.EncodeToString(macBytes), nil +} + +func IsShaEqual(key1, key2 string) bool { + return hmac.Equal([]byte(key1), []byte(key2)) +} diff --git a/types/webhook.go b/types/webhook.go index e701a0fc4..7acee173a 100644 --- a/types/webhook.go +++ b/types/webhook.go @@ -83,6 +83,11 @@ type WebhookCreateInput struct { Triggers []enum.WebhookTrigger `json:"triggers"` } +type WebhookSignatureMetadata struct { + Signature string + BodyBytes []byte +} + type WebhookUpdateInput struct { // TODO [CODE-1363]: remove after identifier migration. UID *string `json:"uid" deprecated:"true"`