mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2025-12-21 08:40:10 -06:00
Feat: OLAP Table for CEL Eval Failures (#2012)
* feat: add table, wire up partitioning * feat: wire failures into the OLAP db from rabbit * feat: bubble failures up to controller * fix: naming * fix: hack around enum type * fix: typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: typos * fix: migration name * feat: log debug failure * feat: pub message from debug endpoint to log failure * fix: error handling * fix: use ingestor * fix: olap suffix * fix: pass source through * fix: dont log ingest failure * fix: rm debug as enum opt * chore: gen * Feat: Webhooks (#1978) * feat: migration + go gen * feat: non unique source name * feat: api types * fix: rm cruft * feat: initial api for webhooks * feat: handle encryption of incoming keys * fix: nil pointer errors * fix: import * feat: add endpoint for incoming webhooks * fix: naming * feat: start wiring up basic auth * feat: wire up cel event parsing * feat: implement authentication * fix: hack for plain text content * feat: add source to enum * feat: add source name enum * feat: db source name enum fix * fix: use source name enums * feat: nest sources * feat: first pass at stripe * fix: clean up source name passing * fix: use unique name for webhook * feat: populator test * fix: null values * fix: ordering * fix: rm unnecessary index * fix: validation * feat: validation on create * fix: lint * fix: naming * feat: wire triggering webhook name through to events table * feat: cleanup + python gen + e2e test for basic auth * feat: query to insert webhook validation errors * refactor: auth handler * fix: naming * refactor: validation errors, part II * feat: wire up writes through olap * fix: linting, fallthrough case * fix: validation * feat: tests for failure cases for basic auth * feat: expand tests * fix: correctly return 404 out of task getter * chore: generated stuff * fix: rm cruft * fix: longer sleep * debug: print name + events to logs * feat: limit to N * feat: add limit env var * debug: ci test * fix: apply namespaces to keys * fix: namespacing, part ii * fix: sdk config * fix: handle prefixing * feat: handle partitioning logic * chore: gen * feat: add webhook limit * feat: wire up limits * fix: gen * fix: reverse order of generic fallthrough * fix: comment for potential unexpected behavior * fix: add check constraints, improve error handling * chore: gen * chore: gen * fix: improve naming * feat: scaffold webhooks page * feat: sidebar * feat: first pass at page * feat: improve feedback on UI * feat: initial work on create modal * feat: change default to basic * fix: openapi spec discriminated union * fix: go side * feat: start wiring up placeholders for stripe and github * feat: pre-populated fields for Stripe + Github * feat: add name section * feat: copy improvements, show URL * feat: UI cleanup * fix: check if tenant populator errors * feat: add comments * chore: gen again * fix: default name * fix: styling * fix: improve stripe header processing * feat: docs, part 1 * fix: lint * fix: migration order * feat: implement rate limit per-webhook * feat: comment * feat: clean up docs * chore: gen * fix: migration versions * fix: olap naming * fix: partitions * chore: gen * feat: store webhook cel eval failures properly * fix: pk order * fix: auth tweaks, move fetches out of populator * fix: pgtype.Text instead of string pointer * chore: gen --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -36,6 +36,7 @@ func NewAuthN(config *server.ServerConfig) *AuthN {
|
||||
func (a *AuthN) Middleware(r *middleware.RouteInfo) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := a.authenticate(c, r)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -58,6 +59,7 @@ func (a *AuthN) authenticate(c echo.Context, r *middleware.RouteInfo) error {
|
||||
|
||||
if r.Security.CookieAuth() {
|
||||
cookieErr = a.handleCookieAuth(c)
|
||||
|
||||
c.Set("auth_strategy", "cookie")
|
||||
|
||||
if cookieErr == nil {
|
||||
@@ -73,6 +75,7 @@ func (a *AuthN) authenticate(c echo.Context, r *middleware.RouteInfo) error {
|
||||
|
||||
if r.Security.BearerAuth() {
|
||||
bearerErr = a.handleBearerAuth(c)
|
||||
|
||||
c.Set("auth_strategy", "bearer")
|
||||
|
||||
if bearerErr == nil {
|
||||
|
||||
@@ -32,7 +32,7 @@ func (t *EventService) EventCreate(ctx echo.Context, request gen.EventCreateRequ
|
||||
}
|
||||
}
|
||||
|
||||
newEvent, err := t.config.Ingestor.IngestEvent(ctx.Request().Context(), tenant, request.Body.Key, dataBytes, additionalMetadata, request.Body.Priority, request.Body.Scope)
|
||||
newEvent, err := t.config.Ingestor.IngestEvent(ctx.Request().Context(), tenant, request.Body.Key, dataBytes, additionalMetadata, request.Body.Priority, request.Body.Scope, nil)
|
||||
|
||||
if err != nil {
|
||||
if err == metered.ErrResourceExhausted {
|
||||
|
||||
@@ -64,7 +64,7 @@ func (i *IngestorsService) SnsUpdate(ctx echo.Context, req gen.SnsUpdateRequestO
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
_, err := i.config.Ingestor.IngestEvent(ctx.Request().Context(), tenant, req.Event, body, nil, nil, nil)
|
||||
_, err := i.config.Ingestor.IngestEvent(ctx.Request().Context(), tenant, req.Event, body, nil, nil, nil, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -13,7 +13,13 @@ func (t *TasksService) V1TaskGet(ctx echo.Context, request gen.V1TaskGetRequestO
|
||||
taskInterface := ctx.Get("task")
|
||||
|
||||
if taskInterface == nil {
|
||||
return nil, echo.NewHTTPError(404, "Task not found")
|
||||
return gen.V1TaskGet404JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: "task not found",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
task, ok := taskInterface.(*sqlcv1.V1TasksOlap)
|
||||
|
||||
184
api/v1/server/handlers/v1/webhooks/create.go
Normal file
184
api/v1/server/handlers/v1/webhooks/create.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package webhooksv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/postgres/dbsqlc"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/postgres/sqlchelpers"
|
||||
v1 "github.com/hatchet-dev/hatchet/pkg/repository/v1"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/v1/sqlcv1"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (w *V1WebhooksService) V1WebhookCreate(ctx echo.Context, request gen.V1WebhookCreateRequestObject) (gen.V1WebhookCreateResponseObject, error) {
|
||||
tenant := ctx.Get("tenant").(*dbsqlc.Tenant)
|
||||
|
||||
canCreate, _, err := w.config.EntitlementRepository.TenantLimit().CanCreate(ctx.Request().Context(), dbsqlc.LimitResourceINCOMINGWEBHOOK, tenant.ID.String(), 1)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if webhook can be created: %w", err)
|
||||
}
|
||||
|
||||
if !canCreate {
|
||||
return gen.V1WebhookCreate400JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: "incoming webhook limit reached",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
params, err := w.constructCreateOpts(tenant.ID.String(), *request.Body)
|
||||
if err != nil {
|
||||
return gen.V1WebhookCreate400JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: fmt.Sprintf("failed to construct webhook create params: %v", err),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
webhook, err := w.config.V1.Webhooks().CreateWebhook(
|
||||
ctx.Request().Context(),
|
||||
tenant.ID.String(),
|
||||
params,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create webhook")
|
||||
}
|
||||
|
||||
transformed := transformers.ToV1Webhook(webhook)
|
||||
|
||||
return gen.V1WebhookCreate200JSONResponse(transformed), nil
|
||||
}
|
||||
|
||||
func extractAuthType(request gen.V1CreateWebhookRequest) (sqlcv1.V1IncomingWebhookAuthType, error) {
|
||||
j, err := request.MarshalJSON()
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get marshal request: %w", err)
|
||||
}
|
||||
|
||||
parsedBody := make(map[string]interface{})
|
||||
if err := json.Unmarshal(j, &parsedBody); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal request body: %w", err)
|
||||
}
|
||||
|
||||
unparsedDiscriminator, ok := parsedBody["authType"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("authType field is missing in the request body")
|
||||
}
|
||||
|
||||
discriminator := unparsedDiscriminator.(string)
|
||||
|
||||
authType := sqlcv1.V1IncomingWebhookAuthType(discriminator)
|
||||
if authType == "" {
|
||||
return "", fmt.Errorf("invalid auth type: %s", unparsedDiscriminator)
|
||||
}
|
||||
|
||||
return authType, nil
|
||||
}
|
||||
|
||||
func (w *V1WebhooksService) constructCreateOpts(tenantId string, request gen.V1CreateWebhookRequest) (v1.CreateWebhookOpts, error) {
|
||||
params := v1.CreateWebhookOpts{
|
||||
Tenantid: sqlchelpers.UUIDFromStr(tenantId),
|
||||
}
|
||||
|
||||
discriminator, err := extractAuthType(request)
|
||||
|
||||
if err != nil {
|
||||
return params, fmt.Errorf("failed to get discriminator: %w", err)
|
||||
}
|
||||
|
||||
authConfig := v1.AuthConfig{}
|
||||
|
||||
switch discriminator {
|
||||
case sqlcv1.V1IncomingWebhookAuthTypeBASIC:
|
||||
basicAuth, err := request.AsV1CreateWebhookRequestBasicAuth()
|
||||
if err != nil {
|
||||
return params, fmt.Errorf("failed to parse basic auth: %w", err)
|
||||
}
|
||||
|
||||
authConfig.Type = sqlcv1.V1IncomingWebhookAuthTypeBASIC
|
||||
|
||||
passwordEncrypted, err := w.config.Encryption.Encrypt([]byte(basicAuth.Auth.Password), "v1_webhook_basic_auth_password")
|
||||
|
||||
if err != nil {
|
||||
return params, fmt.Errorf("failed to encrypt basic auth password: %s", err.Error())
|
||||
}
|
||||
|
||||
authConfig.BasicAuth = &v1.BasicAuthCredentials{
|
||||
Username: basicAuth.Auth.Username,
|
||||
EncryptedPassword: passwordEncrypted,
|
||||
}
|
||||
|
||||
params.Sourcename = sqlcv1.V1IncomingWebhookSourceName(basicAuth.SourceName)
|
||||
params.Name = basicAuth.Name
|
||||
params.Eventkeyexpression = basicAuth.EventKeyExpression
|
||||
params.AuthConfig = authConfig
|
||||
case sqlcv1.V1IncomingWebhookAuthTypeAPIKEY:
|
||||
apiKeyAuth, err := request.AsV1CreateWebhookRequestAPIKey()
|
||||
if err != nil {
|
||||
return params, fmt.Errorf("failed to parse api key auth: %w", err)
|
||||
}
|
||||
|
||||
authConfig.Type = sqlcv1.V1IncomingWebhookAuthTypeAPIKEY
|
||||
|
||||
authConfig := v1.AuthConfig{
|
||||
Type: sqlcv1.V1IncomingWebhookAuthTypeAPIKEY,
|
||||
}
|
||||
|
||||
apiKeyEncrypted, err := w.config.Encryption.Encrypt([]byte(apiKeyAuth.Auth.ApiKey), "v1_webhook_api_key")
|
||||
|
||||
if err != nil {
|
||||
return params, fmt.Errorf("failed to encrypt api key: %s", err.Error())
|
||||
}
|
||||
|
||||
authConfig.APIKeyAuth = &v1.APIKeyAuthCredentials{
|
||||
HeaderName: apiKeyAuth.Auth.HeaderName,
|
||||
EncryptedKey: apiKeyEncrypted,
|
||||
}
|
||||
|
||||
params.Sourcename = sqlcv1.V1IncomingWebhookSourceName(apiKeyAuth.SourceName)
|
||||
params.Name = apiKeyAuth.Name
|
||||
params.Eventkeyexpression = apiKeyAuth.EventKeyExpression
|
||||
params.AuthConfig = authConfig
|
||||
case sqlcv1.V1IncomingWebhookAuthTypeHMAC:
|
||||
hmacAuth, err := request.AsV1CreateWebhookRequestHMAC()
|
||||
if err != nil {
|
||||
return params, fmt.Errorf("failed to parse hmac auth: %w", err)
|
||||
}
|
||||
|
||||
authConfig := v1.AuthConfig{
|
||||
Type: sqlcv1.V1IncomingWebhookAuthTypeHMAC,
|
||||
}
|
||||
|
||||
signingSecretEncrypted, err := w.config.Encryption.Encrypt([]byte(hmacAuth.Auth.SigningSecret), "v1_webhook_hmac_signing_secret")
|
||||
|
||||
if err != nil {
|
||||
return params, fmt.Errorf("failed to encrypt api key: %s", err.Error())
|
||||
}
|
||||
|
||||
authConfig.HMACAuth = &v1.HMACAuthCredentials{
|
||||
Algorithm: sqlcv1.V1IncomingWebhookHmacAlgorithm(hmacAuth.Auth.Algorithm),
|
||||
Encoding: sqlcv1.V1IncomingWebhookHmacEncoding(hmacAuth.Auth.Encoding),
|
||||
SignatureHeaderName: hmacAuth.Auth.SignatureHeaderName,
|
||||
EncryptedWebhookSigningSecret: signingSecretEncrypted,
|
||||
}
|
||||
|
||||
params.Sourcename = sqlcv1.V1IncomingWebhookSourceName(hmacAuth.SourceName)
|
||||
params.Name = hmacAuth.Name
|
||||
params.Eventkeyexpression = hmacAuth.EventKeyExpression
|
||||
params.AuthConfig = authConfig
|
||||
default:
|
||||
return params, fmt.Errorf("unsupported auth type: %s", discriminator)
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
29
api/v1/server/handlers/v1/webhooks/delete.go
Normal file
29
api/v1/server/handlers/v1/webhooks/delete.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package webhooksv1
|
||||
|
||||
import (
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/apierrors"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/v1/sqlcv1"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (w *V1WebhooksService) V1WebhookDelete(ctx echo.Context, request gen.V1WebhookDeleteRequestObject) (gen.V1WebhookDeleteResponseObject, error) {
|
||||
webhook := ctx.Get("v1-webhook").(*sqlcv1.V1IncomingWebhook)
|
||||
|
||||
webhook, err := w.config.V1.Webhooks().DeleteWebhook(
|
||||
ctx.Request().Context(),
|
||||
webhook.TenantID.String(),
|
||||
webhook.Name,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return gen.V1WebhookDelete400JSONResponse(apierrors.NewAPIErrors("failed to delete webhook")), nil
|
||||
}
|
||||
|
||||
transformed := transformers.ToV1Webhook(webhook)
|
||||
|
||||
return gen.V1WebhookDelete200JSONResponse(
|
||||
transformed,
|
||||
), nil
|
||||
}
|
||||
18
api/v1/server/handlers/v1/webhooks/get.go
Normal file
18
api/v1/server/handlers/v1/webhooks/get.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package webhooksv1
|
||||
|
||||
import (
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/v1/sqlcv1"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (w *V1WebhooksService) V1WebhookGet(ctx echo.Context, request gen.V1WebhookGetRequestObject) (gen.V1WebhookGetResponseObject, error) {
|
||||
webhook := ctx.Get("v1-webhook").(*sqlcv1.V1IncomingWebhook)
|
||||
|
||||
transformed := transformers.ToV1Webhook(webhook)
|
||||
|
||||
return gen.V1WebhookGet200JSONResponse(
|
||||
transformed,
|
||||
), nil
|
||||
}
|
||||
49
api/v1/server/handlers/v1/webhooks/list.go
Normal file
49
api/v1/server/handlers/v1/webhooks/list.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package webhooksv1
|
||||
|
||||
import (
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/apierrors"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/postgres/dbsqlc"
|
||||
v1 "github.com/hatchet-dev/hatchet/pkg/repository/v1"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/v1/sqlcv1"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (w *V1WebhooksService) V1WebhookList(ctx echo.Context, request gen.V1WebhookListRequestObject) (gen.V1WebhookListResponseObject, error) {
|
||||
tenant := ctx.Get("tenant").(*dbsqlc.Tenant)
|
||||
|
||||
var sourceNames []sqlcv1.V1IncomingWebhookSourceName
|
||||
var webhookNames []string
|
||||
|
||||
if request.Params.SourceNames != nil {
|
||||
for _, sourceName := range *request.Params.SourceNames {
|
||||
sourceNames = append(sourceNames, sqlcv1.V1IncomingWebhookSourceName(sourceName))
|
||||
}
|
||||
}
|
||||
|
||||
if request.Params.WebhookNames != nil {
|
||||
webhookNames = *request.Params.WebhookNames
|
||||
}
|
||||
|
||||
webhooks, err := w.config.V1.Webhooks().ListWebhooks(
|
||||
ctx.Request().Context(),
|
||||
tenant.ID.String(),
|
||||
v1.ListWebhooksOpts{
|
||||
WebhookNames: webhookNames,
|
||||
WebhookSourceNames: sourceNames,
|
||||
Limit: request.Params.Limit,
|
||||
Offset: request.Params.Offset,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return gen.V1WebhookList400JSONResponse(apierrors.NewAPIErrors("failed to list webhooks")), nil
|
||||
}
|
||||
|
||||
transformed := transformers.ToV1WebhookList(webhooks)
|
||||
|
||||
return gen.V1WebhookList200JSONResponse(
|
||||
transformed,
|
||||
), nil
|
||||
}
|
||||
459
api/v1/server/handlers/v1/webhooks/receive.go
Normal file
459
api/v1/server/handlers/v1/webhooks/receive.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package webhooksv1
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/internal/cel"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/v1/sqlcv1"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (w *V1WebhooksService) V1WebhookReceive(ctx echo.Context, request gen.V1WebhookReceiveRequestObject) (gen.V1WebhookReceiveResponseObject, error) {
|
||||
tenantId := request.Tenant.String()
|
||||
webhookName := request.V1Webhook
|
||||
|
||||
tenant, err := w.config.APIRepository.Tenant().GetTenantByID(ctx.Request().Context(), tenantId)
|
||||
|
||||
if err != nil || tenant == nil {
|
||||
return gen.V1WebhookReceive400JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: "tenant not found",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
webhook, err := w.config.V1.Webhooks().GetWebhook(ctx.Request().Context(), tenantId, webhookName)
|
||||
|
||||
if err != nil || webhook == nil {
|
||||
return gen.V1WebhookReceive400JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: fmt.Sprintf("webhook %s not found for tenant %s", webhookName, tenantId),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if webhook.TenantID.String() != tenantId {
|
||||
return gen.V1WebhookReceive403JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: fmt.Sprintf("webhook %s does not belong to tenant %s", webhookName, tenantId),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
rawBody, err := io.ReadAll(ctx.Request().Body)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read request body: %w", err)
|
||||
}
|
||||
|
||||
ok, validationError := w.validateWebhook(rawBody, *webhook, *ctx.Request())
|
||||
|
||||
if !ok {
|
||||
err := w.config.Ingestor.IngestWebhookValidationFailure(
|
||||
ctx.Request().Context(),
|
||||
tenant,
|
||||
webhook.Name,
|
||||
validationError.ErrorText,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to ingest webhook validation failure: %w", err)
|
||||
}
|
||||
|
||||
return validationError.ToResponse()
|
||||
}
|
||||
|
||||
payloadMap := make(map[string]interface{})
|
||||
|
||||
if rawBody != nil {
|
||||
err := json.Unmarshal(rawBody, &payloadMap)
|
||||
|
||||
if err != nil {
|
||||
return gen.V1WebhookReceive400JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: fmt.Sprintf("failed to unmarshal request body: %v", err),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This could cause unexpected behavior if the payload contains a key named "tenant" or "v1-webhook"
|
||||
delete(payloadMap, "tenant")
|
||||
delete(payloadMap, "v1-webhook")
|
||||
}
|
||||
|
||||
eventKey, err := w.celParser.EvaluateIncomingWebhookExpression(webhook.EventKeyExpression, cel.NewInput(
|
||||
cel.WithInput(payloadMap),
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if eventKey == "" {
|
||||
err = fmt.Errorf("event key evaluted to an empty string")
|
||||
}
|
||||
|
||||
ingestionErr := w.config.Ingestor.IngestCELEvaluationFailure(
|
||||
ctx.Request().Context(),
|
||||
tenant.ID.String(),
|
||||
err.Error(),
|
||||
sqlcv1.V1CelEvaluationFailureSourceWEBHOOK,
|
||||
)
|
||||
|
||||
if ingestionErr != nil {
|
||||
return nil, fmt.Errorf("failed to ingest webhook validation failure: %w", ingestionErr)
|
||||
}
|
||||
|
||||
return gen.V1WebhookReceive400JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: fmt.Sprintf("failed to evaluate event key expression: %v", err),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(payloadMap)
|
||||
if err != nil {
|
||||
return gen.V1WebhookReceive400JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: fmt.Sprintf("failed to marshal request body: %v", err),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
_, err = w.config.Ingestor.IngestEvent(
|
||||
ctx.Request().Context(),
|
||||
tenant,
|
||||
eventKey,
|
||||
payload,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
&webhook.Name,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to ingest event")
|
||||
}
|
||||
|
||||
msg := "ok"
|
||||
|
||||
return gen.V1WebhookReceive200JSONResponse(
|
||||
gen.V1WebhookReceive200JSONResponse{
|
||||
Message: &msg,
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func computeHMACSignature(payload []byte, secret []byte, algorithm sqlcv1.V1IncomingWebhookHmacAlgorithm, encoding sqlcv1.V1IncomingWebhookHmacEncoding) (string, error) {
|
||||
var hashFunc func() hash.Hash
|
||||
switch algorithm {
|
||||
case sqlcv1.V1IncomingWebhookHmacAlgorithmSHA1:
|
||||
hashFunc = sha1.New
|
||||
case sqlcv1.V1IncomingWebhookHmacAlgorithmSHA256:
|
||||
hashFunc = sha256.New
|
||||
case sqlcv1.V1IncomingWebhookHmacAlgorithmSHA512:
|
||||
hashFunc = sha512.New
|
||||
case sqlcv1.V1IncomingWebhookHmacAlgorithmMD5:
|
||||
hashFunc = md5.New
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported HMAC algorithm: %s", algorithm)
|
||||
}
|
||||
|
||||
h := hmac.New(hashFunc, secret)
|
||||
h.Write(payload)
|
||||
signature := h.Sum(nil)
|
||||
|
||||
switch encoding {
|
||||
case sqlcv1.V1IncomingWebhookHmacEncodingHEX:
|
||||
return hex.EncodeToString(signature), nil
|
||||
case sqlcv1.V1IncomingWebhookHmacEncodingBASE64:
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
case sqlcv1.V1IncomingWebhookHmacEncodingBASE64URL:
|
||||
return base64.URLEncoding.EncodeToString(signature), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported HMAC encoding: %s", encoding)
|
||||
}
|
||||
}
|
||||
|
||||
type HttpResponseCode int
|
||||
|
||||
const (
|
||||
Http400 HttpResponseCode = iota
|
||||
Http403
|
||||
Http500
|
||||
)
|
||||
|
||||
type ValidationError struct {
|
||||
Code HttpResponseCode
|
||||
ErrorText string
|
||||
}
|
||||
|
||||
func (vr ValidationError) ToResponse() (gen.V1WebhookReceiveResponseObject, error) {
|
||||
switch vr.Code {
|
||||
case Http400:
|
||||
return gen.V1WebhookReceive400JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: vr.ErrorText,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case Http403:
|
||||
return gen.V1WebhookReceive403JSONResponse{
|
||||
Errors: []gen.APIError{
|
||||
{
|
||||
Description: vr.ErrorText,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case Http500:
|
||||
return nil, errors.New(vr.ErrorText)
|
||||
default:
|
||||
return nil, fmt.Errorf("no validation error set")
|
||||
}
|
||||
}
|
||||
|
||||
func (w *V1WebhooksService) validateWebhook(webhookPayload []byte, webhook sqlcv1.V1IncomingWebhook, request http.Request) (
|
||||
bool,
|
||||
*ValidationError,
|
||||
) {
|
||||
switch webhook.SourceName {
|
||||
case sqlcv1.V1IncomingWebhookSourceNameSTRIPE:
|
||||
signatureHeader := request.Header.Get(webhook.AuthHmacSignatureHeaderName.String)
|
||||
|
||||
if signatureHeader == "" {
|
||||
return false, &ValidationError{
|
||||
Code: Http400,
|
||||
ErrorText: fmt.Sprintf("missing or invalid signature header: %s", webhook.AuthHmacSignatureHeaderName.String),
|
||||
}
|
||||
}
|
||||
|
||||
splitHeader := strings.Split(signatureHeader, ",")
|
||||
headersMap := make(map[string]string)
|
||||
|
||||
for _, header := range splitHeader {
|
||||
parts := strings.Split(header, "=")
|
||||
if len(parts) != 2 {
|
||||
return false, &ValidationError{
|
||||
Code: Http400,
|
||||
ErrorText: fmt.Sprintf("invalid signature header format: %s", webhook.AuthHmacSignatureHeaderName.String),
|
||||
}
|
||||
}
|
||||
headersMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
timestampHeader, hasTimestampHeader := headersMap["t"]
|
||||
v1SignatureHeader, hasV1SignatureHeader := headersMap["v1"]
|
||||
|
||||
if timestampHeader == "" || v1SignatureHeader == "" || !hasTimestampHeader || !hasV1SignatureHeader {
|
||||
return false, &ValidationError{
|
||||
Code: Http400,
|
||||
ErrorText: fmt.Sprintf("missing or invalid signature header: %s", webhook.AuthHmacSignatureHeaderName.String),
|
||||
}
|
||||
}
|
||||
|
||||
timestamp := strings.TrimPrefix(timestampHeader, "t=")
|
||||
signature := strings.TrimPrefix(v1SignatureHeader, "v1=")
|
||||
|
||||
if timestamp == "" || signature == "" {
|
||||
return false, &ValidationError{
|
||||
Code: Http400,
|
||||
ErrorText: fmt.Sprintf("missing or invalid signature header: %s", webhook.AuthHmacSignatureHeaderName.String),
|
||||
}
|
||||
}
|
||||
|
||||
parsedTimestamp, err := strconv.ParseInt(timestamp, 10, 64)
|
||||
|
||||
if err != nil {
|
||||
return false, &ValidationError{
|
||||
Code: Http400,
|
||||
ErrorText: fmt.Sprintf("invalid timestamp in signature header: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
if time.Unix(parsedTimestamp, 0).UTC().Before(time.Now().Add(-10 * time.Minute)) {
|
||||
return false, &ValidationError{
|
||||
Code: Http400,
|
||||
ErrorText: "timestamp in signature header is out of range",
|
||||
}
|
||||
}
|
||||
|
||||
decryptedSigningSecret, err := w.config.Encryption.Decrypt(webhook.AuthHmacWebhookSigningSecret, "v1_webhook_hmac_signing_secret")
|
||||
if err != nil {
|
||||
return false, &ValidationError{
|
||||
Code: Http500,
|
||||
ErrorText: fmt.Sprintf("failed to decrypt HMAC signing secret: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
algorithm := webhook.AuthHmacAlgorithm.V1IncomingWebhookHmacAlgorithm
|
||||
encoding := webhook.AuthHmacEncoding.V1IncomingWebhookHmacEncoding
|
||||
|
||||
signedPayload := fmt.Sprintf("%s.%s", timestamp, webhookPayload)
|
||||
|
||||
expectedSignature, err := computeHMACSignature([]byte(signedPayload), decryptedSigningSecret, algorithm, encoding)
|
||||
|
||||
if err != nil {
|
||||
return false, &ValidationError{
|
||||
Code: Http403,
|
||||
ErrorText: fmt.Sprintf("failed to compute HMAC signature: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
if !signaturesMatch(signature, expectedSignature) {
|
||||
return false, &ValidationError{
|
||||
Code: Http403,
|
||||
ErrorText: "invalid HMAC signature",
|
||||
}
|
||||
}
|
||||
case sqlcv1.V1IncomingWebhookSourceNameGITHUB:
|
||||
fallthrough
|
||||
case sqlcv1.V1IncomingWebhookSourceNameGENERIC:
|
||||
switch webhook.AuthMethod {
|
||||
case sqlcv1.V1IncomingWebhookAuthTypeBASIC:
|
||||
username, password, ok := request.BasicAuth()
|
||||
|
||||
if !ok {
|
||||
return false, &ValidationError{
|
||||
Code: Http403,
|
||||
ErrorText: "missing or invalid authorization header",
|
||||
}
|
||||
}
|
||||
|
||||
decryptedPassword, err := w.config.Encryption.Decrypt(webhook.AuthBasicPassword, "v1_webhook_basic_auth_password")
|
||||
|
||||
if err != nil {
|
||||
return false, &ValidationError{
|
||||
Code: Http500,
|
||||
ErrorText: fmt.Sprintf("failed to decrypt basic auth password: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
if username != webhook.AuthBasicUsername.String || password != string(decryptedPassword) {
|
||||
return false, &ValidationError{
|
||||
Code: Http403,
|
||||
ErrorText: "invalid basic auth credentials",
|
||||
}
|
||||
}
|
||||
case sqlcv1.V1IncomingWebhookAuthTypeAPIKEY:
|
||||
apiKey := request.Header.Get(webhook.AuthApiKeyHeaderName.String)
|
||||
|
||||
if apiKey == "" {
|
||||
return false, &ValidationError{
|
||||
Code: Http403,
|
||||
ErrorText: fmt.Sprintf("missing or invalid api key header: %s", webhook.AuthApiKeyHeaderName.String),
|
||||
}
|
||||
}
|
||||
|
||||
decryptedApiKey, err := w.config.Encryption.Decrypt(webhook.AuthApiKeyKey, "v1_webhook_api_key")
|
||||
|
||||
if err != nil {
|
||||
return false, &ValidationError{
|
||||
Code: Http500,
|
||||
ErrorText: fmt.Sprintf("failed to decrypt api key: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
if apiKey != string(decryptedApiKey) {
|
||||
return false, &ValidationError{
|
||||
Code: Http403,
|
||||
ErrorText: fmt.Sprintf("invalid api key: %s", webhook.AuthApiKeyHeaderName.String),
|
||||
}
|
||||
}
|
||||
case sqlcv1.V1IncomingWebhookAuthTypeHMAC:
|
||||
signature := request.Header.Get(webhook.AuthHmacSignatureHeaderName.String)
|
||||
|
||||
if signature == "" {
|
||||
return false, &ValidationError{
|
||||
Code: Http403,
|
||||
ErrorText: fmt.Sprintf("missing or invalid signature header: %s", webhook.AuthHmacSignatureHeaderName.String),
|
||||
}
|
||||
}
|
||||
|
||||
decryptedSigningSecret, err := w.config.Encryption.Decrypt(webhook.AuthHmacWebhookSigningSecret, "v1_webhook_hmac_signing_secret")
|
||||
|
||||
if err != nil {
|
||||
return false, &ValidationError{
|
||||
Code: Http500,
|
||||
ErrorText: fmt.Sprintf("failed to decrypt HMAC signing secret: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
algorithm := webhook.AuthHmacAlgorithm.V1IncomingWebhookHmacAlgorithm
|
||||
encoding := webhook.AuthHmacEncoding.V1IncomingWebhookHmacEncoding
|
||||
|
||||
expectedSignature, err := computeHMACSignature(webhookPayload, decryptedSigningSecret, algorithm, encoding)
|
||||
|
||||
if err != nil {
|
||||
return false, &ValidationError{
|
||||
Code: Http500,
|
||||
ErrorText: fmt.Sprintf("failed to compute HMAC signature: %s", err),
|
||||
}
|
||||
}
|
||||
|
||||
if !signaturesMatch(signature, expectedSignature) {
|
||||
return false, &ValidationError{
|
||||
Code: Http403,
|
||||
ErrorText: "invalid HMAC signature",
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false, &ValidationError{
|
||||
Code: Http400,
|
||||
ErrorText: fmt.Sprintf("unsupported auth type: %s", webhook.AuthMethod),
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false, &ValidationError{
|
||||
Code: Http400,
|
||||
ErrorText: fmt.Sprintf("unsupported source name: %+v", webhook.SourceName),
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func signaturesMatch(providedSignature, expectedSignature string) bool {
|
||||
providedSignature = strings.TrimSpace(providedSignature)
|
||||
expectedSignature = strings.TrimSpace(expectedSignature)
|
||||
|
||||
return hmac.Equal(
|
||||
[]byte(removePrefixesFromSignature(providedSignature)),
|
||||
[]byte(removePrefixesFromSignature(expectedSignature)),
|
||||
)
|
||||
}
|
||||
|
||||
func removePrefixesFromSignature(signature string) string {
|
||||
signature = strings.TrimPrefix(signature, "sha1=")
|
||||
signature = strings.TrimPrefix(signature, "sha256=")
|
||||
signature = strings.TrimPrefix(signature, "sha512=")
|
||||
signature = strings.TrimPrefix(signature, "md5=")
|
||||
|
||||
return signature
|
||||
}
|
||||
18
api/v1/server/handlers/v1/webhooks/service.go
Normal file
18
api/v1/server/handlers/v1/webhooks/service.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package webhooksv1
|
||||
|
||||
import (
|
||||
"github.com/hatchet-dev/hatchet/internal/cel"
|
||||
"github.com/hatchet-dev/hatchet/pkg/config/server"
|
||||
)
|
||||
|
||||
type V1WebhooksService struct {
|
||||
config *server.ServerConfig
|
||||
celParser *cel.CELParser
|
||||
}
|
||||
|
||||
func NewV1WebhooksService(config *server.ServerConfig) *V1WebhooksService {
|
||||
return &V1WebhooksService{
|
||||
config: config,
|
||||
celParser: cel.NewCELParser(),
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,15 @@ func topLevelResourceGetter(config *server.ServerConfig, parentId, id string) (i
|
||||
}, "", nil
|
||||
}
|
||||
|
||||
func invalidParentGetter(config *server.ServerConfig, parentId, id string) (interface{}, string, error) {
|
||||
newUuid := uuid.NewString()
|
||||
|
||||
return &oneToManyResource{
|
||||
ID: id,
|
||||
ParentID: newUuid,
|
||||
}, newUuid, nil
|
||||
}
|
||||
|
||||
func TestPopulatorMiddleware(t *testing.T) {
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
@@ -78,3 +87,35 @@ func TestPopulatorMiddleware(t *testing.T) {
|
||||
assert.NotNil(t, c.Get("resource1"))
|
||||
assert.NotNil(t, c.Get("resource2"))
|
||||
}
|
||||
|
||||
func TestPopulatorMiddlewareParentDisagreement(t *testing.T) {
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
// Mock RouteInfo
|
||||
routeInfo := &middleware.RouteInfo{
|
||||
Resources: []string{"resource1", "resource2"},
|
||||
}
|
||||
|
||||
resource1Id := uuid.New().String()
|
||||
resource2Id := uuid.New().String()
|
||||
|
||||
// Setting params for the context - NOTE: we need to set both params at once
|
||||
c.SetParamNames("resource1", "resource2")
|
||||
c.SetParamValues(resource1Id, resource2Id)
|
||||
|
||||
// Creating Populator with mock getter function
|
||||
populator := NewPopulator(&server.ServerConfig{})
|
||||
|
||||
populator.RegisterGetter("resource1", topLevelResourceGetter)
|
||||
populator.RegisterGetter("resource2", invalidParentGetter)
|
||||
|
||||
// Using the Populator middleware
|
||||
middlewareFunc := populator.Middleware(routeInfo)
|
||||
err := middlewareFunc(c)
|
||||
|
||||
// Assertions
|
||||
assert.ErrorContains(t, err, "could not be populated")
|
||||
}
|
||||
|
||||
43
api/v1/server/middleware/webhook_rate_limit.go
Normal file
43
api/v1/server/middleware/webhook_rate_limit.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func WebhookRateLimitMiddleware(rateLimit rate.Limit, burst int, l *zerolog.Logger) echo.MiddlewareFunc {
|
||||
config := middleware.RateLimiterConfig{
|
||||
Skipper: func(c echo.Context) bool {
|
||||
method := c.Request().Method
|
||||
|
||||
if method != http.MethodPost {
|
||||
return true
|
||||
}
|
||||
|
||||
tenantId := c.Param("tenant")
|
||||
webhookName := c.Param("v1-webhook")
|
||||
|
||||
if tenantId == "" || webhookName == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return c.Request().URL.Path != fmt.Sprintf("/api/v1/stable/tenants/%s/webhooks/%s", tenantId, webhookName)
|
||||
},
|
||||
|
||||
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
|
||||
middleware.RateLimiterMemoryStoreConfig{
|
||||
Rate: rateLimit,
|
||||
Burst: burst,
|
||||
ExpiresIn: 10 * time.Minute,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
return middleware.RateLimiterWithConfig(config)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,10 +92,11 @@ func ToV1EventList(events []*v1.ListEventsRow, limit, offset, total int64) gen.V
|
||||
Failed: row.FailedCount,
|
||||
Running: row.RunningCount,
|
||||
},
|
||||
Payload: &payload,
|
||||
SeenAt: &row.EventSeenAt.Time,
|
||||
Scope: &row.EventScope,
|
||||
TriggeredRuns: &triggeredRuns,
|
||||
Payload: &payload,
|
||||
SeenAt: &row.EventSeenAt.Time,
|
||||
Scope: &row.EventScope,
|
||||
TriggeredRuns: &triggeredRuns,
|
||||
TriggeringWebhookName: row.TriggeringWebhookName,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
api/v1/server/oas/transformers/v1/webhooks.go
Normal file
37
api/v1/server/oas/transformers/v1/webhooks.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package transformers
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/v1/sqlcv1"
|
||||
)
|
||||
|
||||
func ToV1Webhook(webhook *sqlcv1.V1IncomingWebhook) gen.V1Webhook {
|
||||
// Intentionally empty uuid
|
||||
var id uuid.UUID
|
||||
|
||||
return gen.V1Webhook{
|
||||
AuthType: gen.V1WebhookAuthType(webhook.AuthMethod),
|
||||
Metadata: gen.APIResourceMeta{
|
||||
CreatedAt: webhook.InsertedAt.Time,
|
||||
UpdatedAt: webhook.UpdatedAt.Time,
|
||||
Id: id.String(),
|
||||
},
|
||||
TenantId: webhook.TenantID.String(),
|
||||
EventKeyExpression: webhook.EventKeyExpression,
|
||||
Name: webhook.Name,
|
||||
SourceName: gen.V1WebhookSourceName(webhook.SourceName),
|
||||
}
|
||||
}
|
||||
|
||||
func ToV1WebhookList(webhooks []*sqlcv1.V1IncomingWebhook) gen.V1WebhookList {
|
||||
rows := make([]gen.V1Webhook, len(webhooks))
|
||||
|
||||
for i, webhook := range webhooks {
|
||||
rows[i] = ToV1Webhook(webhook)
|
||||
}
|
||||
|
||||
return gen.V1WebhookList{
|
||||
Rows: &rows,
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
eventsv1 "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1/events"
|
||||
filtersv1 "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1/filters"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1/tasks"
|
||||
webhooksv1 "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1/webhooks"
|
||||
workflowrunsv1 "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1/workflow-runs"
|
||||
webhookworker "github.com/hatchet-dev/hatchet/api/v1/server/handlers/webhook-worker"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/handlers/workers"
|
||||
@@ -41,6 +42,7 @@ import (
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/pkg/config/server"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/postgres/sqlchelpers"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type apiService struct {
|
||||
@@ -64,6 +66,7 @@ type apiService struct {
|
||||
*workflowrunsv1.V1WorkflowRunsService
|
||||
*eventsv1.V1EventsService
|
||||
*filtersv1.V1FiltersService
|
||||
*webhooksv1.V1WebhooksService
|
||||
*celv1.V1CELService
|
||||
}
|
||||
|
||||
@@ -89,6 +92,7 @@ func newAPIService(config *server.ServerConfig) *apiService {
|
||||
V1WorkflowRunsService: workflowrunsv1.NewV1WorkflowRunsService(config),
|
||||
V1EventsService: eventsv1.NewV1EventsService(config),
|
||||
V1FiltersService: filtersv1.NewV1FiltersService(config),
|
||||
V1WebhooksService: webhooksv1.NewV1WebhooksService(config),
|
||||
V1CELService: celv1.NewV1CELService(config),
|
||||
}
|
||||
}
|
||||
@@ -417,6 +421,20 @@ func (t *APIServer) registerSpec(g *echo.Group, spec *openapi3.T, middlewares []
|
||||
return filter, sqlchelpers.UUIDToStr(filter.TenantID), nil
|
||||
})
|
||||
|
||||
populatorMW.RegisterGetter("v1-webhook", func(config *server.ServerConfig, parentId, id string) (result interface{}, uniqueParentId string, err error) {
|
||||
webhook, err := t.config.V1.Webhooks().GetWebhook(
|
||||
context.Background(),
|
||||
parentId,
|
||||
id,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return webhook, sqlchelpers.UUIDToStr(webhook.TenantID), nil
|
||||
})
|
||||
|
||||
authnMW := authn.NewAuthN(t.config)
|
||||
authzMW := authz.NewAuthZ(t.config)
|
||||
|
||||
@@ -488,6 +506,11 @@ func (t *APIServer) registerSpec(g *echo.Group, spec *openapi3.T, middlewares []
|
||||
loggerMiddleware,
|
||||
middleware.Recover(),
|
||||
allHatchetMiddleware,
|
||||
hatchetmiddleware.WebhookRateLimitMiddleware(
|
||||
rate.Limit(t.config.Runtime.WebhookRateLimit),
|
||||
t.config.Runtime.WebhookRateLimitBurst,
|
||||
t.config.Logger,
|
||||
),
|
||||
)
|
||||
|
||||
return populatorMW, nil
|
||||
|
||||
Reference in New Issue
Block a user