feat: [CODE-2886]: jira auth support (#3119)

BT-10437
Abhinav Singh 2024-12-12 19:36:08 +00:00 committed by Harness
parent 1e18e289d8
commit c7f7ade6b6
13 changed files with 150 additions and 41 deletions

View File

@ -20,8 +20,16 @@ import (
) )
type Preprocessor interface { type Preprocessor interface {
PreprocessCreateInput(enum.PrincipalType, *types.WebhookCreateInput) (enum.WebhookType, error) PreprocessCreateInput(
PreprocessUpdateInput(enum.PrincipalType, *types.WebhookUpdateInput) (enum.WebhookType, error) enum.PrincipalType,
*types.WebhookCreateInput,
*types.WebhookSignatureMetadata,
) (enum.WebhookType, error)
PreprocessUpdateInput(
enum.PrincipalType,
*types.WebhookUpdateInput,
*types.WebhookSignatureMetadata,
) (enum.WebhookType, error)
PreprocessFilter(enum.PrincipalType, *types.WebhookFilter) PreprocessFilter(enum.PrincipalType, *types.WebhookFilter)
IsInternalCall(enum.PrincipalType) bool IsInternalCall(enum.PrincipalType) bool
} }
@ -33,6 +41,7 @@ type NoopPreprocessor struct {
func (p NoopPreprocessor) PreprocessCreateInput( func (p NoopPreprocessor) PreprocessCreateInput(
enum.PrincipalType, enum.PrincipalType,
*types.WebhookCreateInput, *types.WebhookCreateInput,
*types.WebhookSignatureMetadata,
) (enum.WebhookType, error) { ) (enum.WebhookType, error) {
return enum.WebhookTypeExternal, nil return enum.WebhookTypeExternal, nil
} }
@ -41,6 +50,7 @@ func (p NoopPreprocessor) PreprocessCreateInput(
func (p NoopPreprocessor) PreprocessUpdateInput( func (p NoopPreprocessor) PreprocessUpdateInput(
enum.PrincipalType, enum.PrincipalType,
*types.WebhookUpdateInput, *types.WebhookUpdateInput,
*types.WebhookSignatureMetadata,
) (enum.WebhookType, error) { ) (enum.WebhookType, error) {
return enum.WebhookTypeExternal, nil return enum.WebhookTypeExternal, nil
} }

View File

@ -29,13 +29,14 @@ func (c *Controller) CreateRepo(
session *auth.Session, session *auth.Session,
repoRef string, repoRef string,
in *types.WebhookCreateInput, in *types.WebhookCreateInput,
signatureData *types.WebhookSignatureMetadata,
) (*types.Webhook, error) { ) (*types.Webhook, error) {
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to acquire access to the repo: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to preprocess create input: %w", err) return nil, fmt.Errorf("failed to preprocess create input: %w", err)
} }

View File

@ -30,18 +30,17 @@ func (c *Controller) UpdateRepo(
repoRef string, repoRef string,
webhookIdentifier string, webhookIdentifier string,
in *types.WebhookUpdateInput, in *types.WebhookUpdateInput,
signatureData *types.WebhookSignatureMetadata,
) (*types.Webhook, error) { ) (*types.Webhook, error) {
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to acquire access to the repo: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to preprocess update input: %w", err) return nil, fmt.Errorf("failed to preprocess update input: %w", err)
} }
return c.webhookService.Update( return c.webhookService.Update(ctx, repo.ID, enum.WebhookParentRepo, webhookIdentifier, typ, in)
ctx, repo.ID, enum.WebhookParentRepo, webhookIdentifier, typ, in,
)
} }

View File

@ -29,20 +29,19 @@ func (c *Controller) CreateSpace(
session *auth.Session, session *auth.Session,
spaceRef string, spaceRef string,
in *types.WebhookCreateInput, in *types.WebhookCreateInput,
signatureData *types.WebhookSignatureMetadata,
) (*types.Webhook, error) { ) (*types.Webhook, error) {
space, err := c.getSpaceCheckAccess(ctx, session, spaceRef, enum.PermissionSpaceEdit) space, err := c.getSpaceCheckAccess(ctx, session, spaceRef, enum.PermissionSpaceEdit)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to acquire access to space: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to preprocess create input: %w", err) return nil, fmt.Errorf("failed to preprocess create input: %w", err)
} }
hook, err := c.webhookService.Create( hook, err := c.webhookService.Create(ctx, session.Principal.ID, space.ID, enum.WebhookParentSpace, internal, in)
ctx, session.Principal.ID, space.ID, enum.WebhookParentSpace, internal, in,
)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create webhook: %w", err) return nil, fmt.Errorf("failed to create webhook: %w", err)
} }

View File

@ -30,13 +30,14 @@ func (c *Controller) UpdateSpace(
spaceRef string, spaceRef string,
webhookIdentifier string, webhookIdentifier string,
in *types.WebhookUpdateInput, in *types.WebhookUpdateInput,
signatureData *types.WebhookSignatureMetadata,
) (*types.Webhook, error) { ) (*types.Webhook, error) {
space, err := c.getSpaceCheckAccess(ctx, session, spaceRef, enum.PermissionSpaceEdit) space, err := c.getSpaceCheckAccess(ctx, session, spaceRef, enum.PermissionSpaceEdit)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to acquire access to space: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to preprocess update input: %w", err) return nil, fmt.Errorf("failed to preprocess update input: %w", err)
} }

View File

@ -15,7 +15,9 @@
package webhook package webhook
import ( import (
"bytes"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"github.com/harness/gitness/app/api/controller/webhook" "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) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx) 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) repoRef, err := request.GetRepoRefFromPath(r)
if err != nil { if err != nil {
@ -37,13 +44,22 @@ func HandleCreateRepo(webhookCtrl *webhook.Controller) http.HandlerFunc {
} }
in := new(types.WebhookCreateInput) 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 { if err != nil {
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
return 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 { if err != nil {
render.TranslatedUserError(ctx, w, err) render.TranslatedUserError(ctx, w, err)
return return

View File

@ -15,7 +15,9 @@
package webhook package webhook
import ( import (
"bytes"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"github.com/harness/gitness/app/api/controller/webhook" "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) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx) 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) repoRef, err := request.GetRepoRefFromPath(r)
if err != nil { if err != nil {
@ -43,13 +50,22 @@ func HandleUpdateRepo(webhookCtrl *webhook.Controller) http.HandlerFunc {
} }
in := new(types.WebhookUpdateInput) 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 { if err != nil {
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
return 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 { if err != nil {
render.TranslatedUserError(ctx, w, err) render.TranslatedUserError(ctx, w, err)
return return

View File

@ -15,7 +15,9 @@
package webhook package webhook
import ( import (
"bytes"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"github.com/harness/gitness/app/api/controller/webhook" "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) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx) 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) spaceRef, err := request.GetSpaceRefFromPath(r)
if err != nil { if err != nil {
@ -37,13 +44,22 @@ func HandleCreateSpace(webhookCtrl *webhook.Controller) http.HandlerFunc {
} }
in := new(types.WebhookCreateInput) 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 { if err != nil {
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
return 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 { if err != nil {
render.TranslatedUserError(ctx, w, err) render.TranslatedUserError(ctx, w, err)
return return

View File

@ -15,7 +15,9 @@
package webhook package webhook
import ( import (
"bytes"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"github.com/harness/gitness/app/api/controller/webhook" "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) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx) 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) spaceRef, err := request.GetSpaceRefFromPath(r)
if err != nil { if err != nil {
@ -43,13 +50,22 @@ func HandleUpdateSpace(webhookCtrl *webhook.Controller) http.HandlerFunc {
} }
in := new(types.WebhookUpdateInput) 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 { if err != nil {
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
return 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 { if err != nil {
render.TranslatedUserError(ctx, w, err) render.TranslatedUserError(ctx, w, err)
return return

View File

@ -67,6 +67,8 @@ const (
HeaderIfNoneMatch = "If-None-Match" HeaderIfNoneMatch = "If-None-Match"
HeaderETag = "ETag" HeaderETag = "ETag"
HeaderSignature = "Signature"
) )
// GetOptionalRemainderFromPath returns the remainder ("*") from the path or an empty string if it doesn't exist. // 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) { func GetIfNoneMatchFromHeader(r *http.Request) (string, bool) {
return GetHeader(r, HeaderIfNoneMatch) return GetHeader(r, HeaderIfNoneMatch)
} }
func GetSignatureFromHeaderOrDefault(r *http.Request, dflt string) string {
return GetHeaderOrDefault(r, HeaderSignature, dflt)
}

View File

@ -17,9 +17,6 @@ package webhook
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -28,6 +25,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/harness/gitness/crypto"
"github.com/harness/gitness/store" "github.com/harness/gitness/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "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) return nil, fmt.Errorf("failed to decrypt webhook secret: %w", err)
} }
var hmac string var hmac string
hmac, err = generateHMACSHA256(bBuff.Bytes(), []byte(decryptedSecret)) hmac, err = crypto.GenerateHMACSHA256(bBuff.Bytes(), []byte(decryptedSecret))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate SHA256 based HMAC: %w", err) 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) 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
}

43
crypto/crypto.go Normal file
View File

@ -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))
}

View File

@ -83,6 +83,11 @@ type WebhookCreateInput struct {
Triggers []enum.WebhookTrigger `json:"triggers"` Triggers []enum.WebhookTrigger `json:"triggers"`
} }
type WebhookSignatureMetadata struct {
Signature string
BodyBytes []byte
}
type WebhookUpdateInput struct { type WebhookUpdateInput struct {
// TODO [CODE-1363]: remove after identifier migration. // TODO [CODE-1363]: remove after identifier migration.
UID *string `json:"uid" deprecated:"true"` UID *string `json:"uid" deprecated:"true"`