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 {
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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"`
}
type WebhookSignatureMetadata struct {
Signature string
BodyBytes []byte
}
type WebhookUpdateInput struct {
// TODO [CODE-1363]: remove after identifier migration.
UID *string `json:"uid" deprecated:"true"`