From dc495db094cba47606a7aab96de0b839debdd490 Mon Sep 17 00:00:00 2001 From: Thomas Lathuiliere Date: Mon, 15 Jul 2024 18:46:43 +0200 Subject: [PATCH] refactor(webhooks): crud and security --- api-clients/convoy/api.yaml | 22 +- api-clients/convoy/cfg.yaml | 3 + api-clients/convoy/client.gen.go | 734 +++++++++++++++++- api/handle_webhooks.go | 59 +- api/routes.go | 1 + dto/webhooks.go | 27 +- models/webhook.go | 46 +- repositories/convoy_repository.go | 172 +++- usecases/security/enforce_security_webhook.go | 32 +- usecases/webhooks_usecase.go | 47 +- utils/organization_security.go | 9 + 11 files changed, 1048 insertions(+), 104 deletions(-) diff --git a/api-clients/convoy/api.yaml b/api-clients/convoy/api.yaml index 81dcfc82e..9713cccaa 100644 --- a/api-clients/convoy/api.yaml +++ b/api-clients/convoy/api.yaml @@ -361,6 +361,8 @@ components: type: array interval_seconds: type: integer + max_retry_seconds: + type: integer next_send_time: type: string num_trials: @@ -1932,7 +1934,7 @@ paths: required: true x-originalParamName: endpoint responses: - "200": + "202": content: application/json: schema: @@ -1942,7 +1944,7 @@ paths: data: $ref: "#/components/schemas/models.EndpointResponse" type: object - description: OK + description: Accepted "400": content: application/json: @@ -2076,7 +2078,7 @@ paths: schema: type: string responses: - "200": + "202": content: application/json: schema: @@ -2086,7 +2088,7 @@ paths: data: $ref: "#/components/schemas/models.EndpointResponse" type: object - description: OK + description: Accepted "400": content: application/json: @@ -3930,7 +3932,7 @@ paths: required: true x-originalParamName: portallink responses: - "200": + "202": content: application/json: schema: @@ -3940,7 +3942,7 @@ paths: data: $ref: "#/components/schemas/models.PortalLinkResponse" type: object - description: OK + description: Accepted "400": content: application/json: @@ -4380,7 +4382,7 @@ paths: required: true x-originalParamName: source responses: - "200": + "202": content: application/json: schema: @@ -4390,7 +4392,7 @@ paths: data: $ref: "#/components/schemas/models.SourceResponse" type: object - description: OK + description: Accepted "400": content: application/json: @@ -4765,7 +4767,7 @@ paths: required: true x-originalParamName: subscription responses: - "200": + "202": content: application/json: schema: @@ -4775,7 +4777,7 @@ paths: data: $ref: "#/components/schemas/models.SubscriptionResponse" type: object - description: OK + description: Accepted "400": content: application/json: diff --git a/api-clients/convoy/cfg.yaml b/api-clients/convoy/cfg.yaml index b5317d1fe..67ccf69eb 100644 --- a/api-clients/convoy/cfg.yaml +++ b/api-clients/convoy/cfg.yaml @@ -9,6 +9,9 @@ output-options: - CreateEndpointFanoutEvent - CreateEndpoint - CreateSubscription + - GetEndpoint - GetEndpoints - GetSubscriptions - DeleteEndpoint + - UpdateEndpoint + - UpdateSubscription diff --git a/api-clients/convoy/client.gen.go b/api-clients/convoy/client.gen.go index 7c747a166..da6053dfd 100644 --- a/api-clients/convoy/client.gen.go +++ b/api-clients/convoy/client.gen.go @@ -567,6 +567,86 @@ type ModelsSubscriptionResponse struct { UpdatedAt *string `json:"updated_at,omitempty"` } +// ModelsUpdateEndpoint defines model for models.UpdateEndpoint. +type ModelsUpdateEndpoint struct { + // AdvancedSignatures Convoy supports two [signature formats](https://getconvoy.io/docs/manual/signatures) + // -- simple or advanced. If left unspecified, we default to false. + AdvancedSignatures *bool `json:"advanced_signatures,omitempty"` + + // Authentication This is used to define any custom authentication required by the endpoint. This + // shouldn't be needed often because webhook endpoints usually should be exposed to + // the internet. + Authentication *ModelsEndpointAuthentication `json:"authentication,omitempty"` + + // Description Human-readable description of the endpoint. Think of this as metadata describing + // the endpoint + Description *string `json:"description,omitempty"` + + // HttpTimeout Define endpoint http timeout in seconds. + HttpTimeout *int `json:"http_timeout,omitempty"` + + // IsDisabled This is used to manually enable/disable the endpoint. + IsDisabled *bool `json:"is_disabled,omitempty"` + Name *string `json:"name,omitempty"` + + // OwnerId The OwnerID is used to group more than one endpoint together to achieve + // [fanout](https://getconvoy.io/docs/manual/endpoints#Endpoint%20Owner%20ID) + OwnerId *string `json:"owner_id,omitempty"` + + // RateLimit Rate limit is the total number of requests to be sent to an endpoint in + // the time duration specified in RateLimitDuration + RateLimit *int `json:"rate_limit,omitempty"` + + // RateLimitDuration Rate limit duration specifies the time range for the rate limit. + RateLimitDuration *int `json:"rate_limit_duration,omitempty"` + + // Secret Endpoint's webhook secret. If not provided, Convoy autogenerates one for the endpoint. + Secret *string `json:"secret,omitempty"` + + // SlackWebhookUrl Slack webhook URL is an alternative method to support email where endpoint developers + // can receive failure notifications on a slack channel. + SlackWebhookUrl *string `json:"slack_webhook_url,omitempty"` + + // SupportEmail Endpoint developers support email. This is used for communicating endpoint state + // changes. You should always turn this on when disabling endpoints are enabled. + SupportEmail *string `json:"support_email,omitempty"` + + // Url URL is the endpoint's URL prefixed with https. non-https urls are currently + // not supported. + Url *string `json:"url,omitempty"` +} + +// ModelsUpdateSubscription defines model for models.UpdateSubscription. +type ModelsUpdateSubscription struct { + // AlertConfig Alert configuration + AlertConfig *ModelsAlertConfiguration `json:"alert_config,omitempty"` + + // AppId Deprecated but necessary for backward compatibility + AppId *string `json:"app_id,omitempty"` + + // EndpointId Destination endpoint ID + EndpointId *string `json:"endpoint_id,omitempty"` + + // FilterConfig Filter configuration + FilterConfig *ModelsFilterConfiguration `json:"filter_config,omitempty"` + + // Function Convoy supports mutating your request payload using a js function. Use this field + // to specify a `transform` function for this purpose. See this[https://docs.getconvoy.io/product-manual/subscriptions#functions] for more + Function *string `json:"function,omitempty"` + + // Name Subscription Nme + Name *string `json:"name,omitempty"` + + // RateLimitConfig Rate limit configuration + RateLimitConfig *ModelsRateLimitConfiguration `json:"rate_limit_config,omitempty"` + + // RetryConfig Retry configuration + RetryConfig *ModelsRetryConfiguration `json:"retry_config,omitempty"` + + // SourceId Source Id + SourceId *string `json:"source_id,omitempty"` +} + // UtilServerResponse defines model for util.ServerResponse. type UtilServerResponse struct { Message *string `json:"message,omitempty"` @@ -628,12 +708,18 @@ type GetSubscriptionsParamsDirection string // CreateEndpointJSONRequestBody defines body for CreateEndpoint for application/json ContentType. type CreateEndpointJSONRequestBody = ModelsCreateEndpoint +// UpdateEndpointJSONRequestBody defines body for UpdateEndpoint for application/json ContentType. +type UpdateEndpointJSONRequestBody = ModelsUpdateEndpoint + // CreateEndpointFanoutEventJSONRequestBody defines body for CreateEndpointFanoutEvent for application/json ContentType. type CreateEndpointFanoutEventJSONRequestBody = ModelsFanoutEvent // CreateSubscriptionJSONRequestBody defines body for CreateSubscription for application/json ContentType. type CreateSubscriptionJSONRequestBody = ModelsCreateSubscription +// UpdateSubscriptionJSONRequestBody defines body for UpdateSubscription for application/json ContentType. +type UpdateSubscriptionJSONRequestBody = ModelsUpdateSubscription + // RequestEditorFn is the function signature for the RequestEditor callback function type RequestEditorFn func(ctx context.Context, req *http.Request) error @@ -718,6 +804,14 @@ type ClientInterface interface { // DeleteEndpoint request DeleteEndpoint(ctx context.Context, projectID string, endpointID string, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetEndpoint request + GetEndpoint(ctx context.Context, projectID string, endpointID string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UpdateEndpointWithBody request with any body + UpdateEndpointWithBody(ctx context.Context, projectID string, endpointID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpdateEndpoint(ctx context.Context, projectID string, endpointID string, body UpdateEndpointJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateEndpointFanoutEventWithBody request with any body CreateEndpointFanoutEventWithBody(ctx context.Context, projectID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -730,6 +824,11 @@ type ClientInterface interface { CreateSubscriptionWithBody(ctx context.Context, projectID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) CreateSubscription(ctx context.Context, projectID string, body CreateSubscriptionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UpdateSubscriptionWithBody request with any body + UpdateSubscriptionWithBody(ctx context.Context, projectID string, subscriptionID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpdateSubscription(ctx context.Context, projectID string, subscriptionID string, body UpdateSubscriptionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) GetEndpoints(ctx context.Context, projectID string, params *GetEndpointsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -780,6 +879,42 @@ func (c *Client) DeleteEndpoint(ctx context.Context, projectID string, endpointI return c.Client.Do(req) } +func (c *Client) GetEndpoint(ctx context.Context, projectID string, endpointID string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetEndpointRequest(c.Server, projectID, endpointID) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateEndpointWithBody(ctx context.Context, projectID string, endpointID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateEndpointRequestWithBody(c.Server, projectID, endpointID, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateEndpoint(ctx context.Context, projectID string, endpointID string, body UpdateEndpointJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateEndpointRequest(c.Server, projectID, endpointID, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) CreateEndpointFanoutEventWithBody(ctx context.Context, projectID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewCreateEndpointFanoutEventRequestWithBody(c.Server, projectID, contentType, body) if err != nil { @@ -840,6 +975,30 @@ func (c *Client) CreateSubscription(ctx context.Context, projectID string, body return c.Client.Do(req) } +func (c *Client) UpdateSubscriptionWithBody(ctx context.Context, projectID string, subscriptionID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateSubscriptionRequestWithBody(c.Server, projectID, subscriptionID, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateSubscription(ctx context.Context, projectID string, subscriptionID string, body UpdateSubscriptionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateSubscriptionRequest(c.Server, projectID, subscriptionID, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + // NewGetEndpointsRequest generates requests for GetEndpoints func NewGetEndpointsRequest(server string, projectID string, params *GetEndpointsParams) (*http.Request, error) { var err error @@ -1080,6 +1239,101 @@ func NewDeleteEndpointRequest(server string, projectID string, endpointID string return req, nil } +// NewGetEndpointRequest generates requests for GetEndpoint +func NewGetEndpointRequest(server string, projectID string, endpointID string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "endpointID", runtime.ParamLocationPath, endpointID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/endpoints/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewUpdateEndpointRequest calls the generic UpdateEndpoint builder with application/json body +func NewUpdateEndpointRequest(server string, projectID string, endpointID string, body UpdateEndpointJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewUpdateEndpointRequestWithBody(server, projectID, endpointID, "application/json", bodyReader) +} + +// NewUpdateEndpointRequestWithBody generates requests for UpdateEndpoint with any type of body +func NewUpdateEndpointRequestWithBody(server string, projectID string, endpointID string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "endpointID", runtime.ParamLocationPath, endpointID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/endpoints/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewCreateEndpointFanoutEventRequest calls the generic CreateEndpointFanoutEvent builder with application/json body func NewCreateEndpointFanoutEventRequest(server string, projectID string, body CreateEndpointFanoutEventJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1326,43 +1580,97 @@ func NewCreateSubscriptionRequestWithBody(server string, projectID string, conte return req, nil } -func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range c.RequestEditors { - if err := r(ctx, req); err != nil { - return err - } - } - for _, r := range additionalEditors { - if err := r(ctx, req); err != nil { - return err - } +// NewUpdateSubscriptionRequest calls the generic UpdateSubscription builder with application/json body +func NewUpdateSubscriptionRequest(server string, projectID string, subscriptionID string, body UpdateSubscriptionJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err } - return nil + bodyReader = bytes.NewReader(buf) + return NewUpdateSubscriptionRequestWithBody(server, projectID, subscriptionID, "application/json", bodyReader) } -// ClientWithResponses builds on ClientInterface to offer response payloads -type ClientWithResponses struct { - ClientInterface -} +// NewUpdateSubscriptionRequestWithBody generates requests for UpdateSubscription with any type of body +func NewUpdateSubscriptionRequestWithBody(server string, projectID string, subscriptionID string, contentType string, body io.Reader) (*http.Request, error) { + var err error -// NewClientWithResponses creates a new ClientWithResponses, which wraps -// Client with return type handling -func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { - client, err := NewClient(server, opts...) + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) if err != nil { return nil, err } - return &ClientWithResponses{client}, nil -} -// WithBaseURL overrides the baseURL. -func WithBaseURL(baseURL string) ClientOption { - return func(c *Client) error { - newBaseURL, err := url.Parse(baseURL) - if err != nil { - return err - } - c.Server = newBaseURL.String() + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "subscriptionID", runtime.ParamLocationPath, subscriptionID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/projects/%s/subscriptions/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() return nil } } @@ -1380,6 +1688,14 @@ type ClientWithResponsesInterface interface { // DeleteEndpointWithResponse request DeleteEndpointWithResponse(ctx context.Context, projectID string, endpointID string, reqEditors ...RequestEditorFn) (*DeleteEndpointResponse, error) + // GetEndpointWithResponse request + GetEndpointWithResponse(ctx context.Context, projectID string, endpointID string, reqEditors ...RequestEditorFn) (*GetEndpointResponse, error) + + // UpdateEndpointWithBodyWithResponse request with any body + UpdateEndpointWithBodyWithResponse(ctx context.Context, projectID string, endpointID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateEndpointResponse, error) + + UpdateEndpointWithResponse(ctx context.Context, projectID string, endpointID string, body UpdateEndpointJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateEndpointResponse, error) + // CreateEndpointFanoutEventWithBodyWithResponse request with any body CreateEndpointFanoutEventWithBodyWithResponse(ctx context.Context, projectID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateEndpointFanoutEventResponse, error) @@ -1392,6 +1708,11 @@ type ClientWithResponsesInterface interface { CreateSubscriptionWithBodyWithResponse(ctx context.Context, projectID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSubscriptionResponse, error) CreateSubscriptionWithResponse(ctx context.Context, projectID string, body CreateSubscriptionJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateSubscriptionResponse, error) + + // UpdateSubscriptionWithBodyWithResponse request with any body + UpdateSubscriptionWithBodyWithResponse(ctx context.Context, projectID string, subscriptionID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateSubscriptionResponse, error) + + UpdateSubscriptionWithResponse(ctx context.Context, projectID string, subscriptionID string, body UpdateSubscriptionJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateSubscriptionResponse, error) } type GetEndpointsResponse struct { @@ -1520,6 +1841,88 @@ func (r DeleteEndpointResponse) StatusCode() int { return 0 } +type GetEndpointResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Data *ModelsEndpointResponse `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON400 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON401 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON404 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } +} + +// Status returns HTTPResponse.Status +func (r GetEndpointResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetEndpointResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type UpdateEndpointResponse struct { + Body []byte + HTTPResponse *http.Response + JSON202 *struct { + Data *ModelsEndpointResponse `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON400 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON401 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON404 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } +} + +// Status returns HTTPResponse.Status +func (r UpdateEndpointResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpdateEndpointResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type CreateEndpointFanoutEventResponse struct { Body []byte HTTPResponse *http.Response @@ -1646,6 +2049,47 @@ func (r CreateSubscriptionResponse) StatusCode() int { return 0 } +type UpdateSubscriptionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON202 *struct { + Data *ModelsSubscriptionResponse `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON400 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON401 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + JSON404 *struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } +} + +// Status returns HTTPResponse.Status +func (r UpdateSubscriptionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpdateSubscriptionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + // GetEndpointsWithResponse request returning *GetEndpointsResponse func (c *ClientWithResponses) GetEndpointsWithResponse(ctx context.Context, projectID string, params *GetEndpointsParams, reqEditors ...RequestEditorFn) (*GetEndpointsResponse, error) { rsp, err := c.GetEndpoints(ctx, projectID, params, reqEditors...) @@ -1681,6 +2125,32 @@ func (c *ClientWithResponses) DeleteEndpointWithResponse(ctx context.Context, pr return ParseDeleteEndpointResponse(rsp) } +// GetEndpointWithResponse request returning *GetEndpointResponse +func (c *ClientWithResponses) GetEndpointWithResponse(ctx context.Context, projectID string, endpointID string, reqEditors ...RequestEditorFn) (*GetEndpointResponse, error) { + rsp, err := c.GetEndpoint(ctx, projectID, endpointID, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetEndpointResponse(rsp) +} + +// UpdateEndpointWithBodyWithResponse request with arbitrary body returning *UpdateEndpointResponse +func (c *ClientWithResponses) UpdateEndpointWithBodyWithResponse(ctx context.Context, projectID string, endpointID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateEndpointResponse, error) { + rsp, err := c.UpdateEndpointWithBody(ctx, projectID, endpointID, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateEndpointResponse(rsp) +} + +func (c *ClientWithResponses) UpdateEndpointWithResponse(ctx context.Context, projectID string, endpointID string, body UpdateEndpointJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateEndpointResponse, error) { + rsp, err := c.UpdateEndpoint(ctx, projectID, endpointID, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateEndpointResponse(rsp) +} + // CreateEndpointFanoutEventWithBodyWithResponse request with arbitrary body returning *CreateEndpointFanoutEventResponse func (c *ClientWithResponses) CreateEndpointFanoutEventWithBodyWithResponse(ctx context.Context, projectID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateEndpointFanoutEventResponse, error) { rsp, err := c.CreateEndpointFanoutEventWithBody(ctx, projectID, contentType, body, reqEditors...) @@ -1724,6 +2194,23 @@ func (c *ClientWithResponses) CreateSubscriptionWithResponse(ctx context.Context return ParseCreateSubscriptionResponse(rsp) } +// UpdateSubscriptionWithBodyWithResponse request with arbitrary body returning *UpdateSubscriptionResponse +func (c *ClientWithResponses) UpdateSubscriptionWithBodyWithResponse(ctx context.Context, projectID string, subscriptionID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateSubscriptionResponse, error) { + rsp, err := c.UpdateSubscriptionWithBody(ctx, projectID, subscriptionID, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateSubscriptionResponse(rsp) +} + +func (c *ClientWithResponses) UpdateSubscriptionWithResponse(ctx context.Context, projectID string, subscriptionID string, body UpdateSubscriptionJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateSubscriptionResponse, error) { + rsp, err := c.UpdateSubscription(ctx, projectID, subscriptionID, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateSubscriptionResponse(rsp) +} + // ParseGetEndpointsResponse parses an HTTP response from a GetEndpointsWithResponse call func ParseGetEndpointsResponse(rsp *http.Response) (*GetEndpointsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -1916,6 +2403,132 @@ func ParseDeleteEndpointResponse(rsp *http.Response) (*DeleteEndpointResponse, e return response, nil } +// ParseGetEndpointResponse parses an HTTP response from a GetEndpointWithResponse call +func ParseGetEndpointResponse(rsp *http.Response) (*GetEndpointResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetEndpointResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Data *ModelsEndpointResponse `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + +// ParseUpdateEndpointResponse parses an HTTP response from a UpdateEndpointWithResponse call +func ParseUpdateEndpointResponse(rsp *http.Response) (*UpdateEndpointResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UpdateEndpointResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest struct { + Data *ModelsEndpointResponse `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + // ParseCreateEndpointFanoutEventResponse parses an HTTP response from a CreateEndpointFanoutEventWithResponse call func ParseCreateEndpointFanoutEventResponse(rsp *http.Response) (*CreateEndpointFanoutEventResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -2107,3 +2720,66 @@ func ParseCreateSubscriptionResponse(rsp *http.Response) (*CreateSubscriptionRes return response, nil } + +// ParseUpdateSubscriptionResponse parses an HTTP response from a UpdateSubscriptionWithResponse call +func ParseUpdateSubscriptionResponse(rsp *http.Response) (*UpdateSubscriptionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UpdateSubscriptionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest struct { + Data *ModelsSubscriptionResponse `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest struct { + Data *HandlersStub `json:"data,omitempty"` + Message *string `json:"message,omitempty"` + Status *bool `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} diff --git a/api/handle_webhooks.go b/api/handle_webhooks.go index 37a2294fa..8683af06e 100644 --- a/api/handle_webhooks.go +++ b/api/handle_webhooks.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/checkmarble/marble-backend/dto" + "github.com/checkmarble/marble-backend/models" "github.com/checkmarble/marble-backend/pure_utils" "github.com/checkmarble/marble-backend/utils" "github.com/guregu/null/v5" @@ -32,12 +33,40 @@ func (api *API) handleListWebhooks(c *gin.Context) { } func (api *API) handleRegisterWebhook(c *gin.Context) { + creds, found := utils.CredentialsFromCtx(c.Request.Context()) + if !found { + presentError(c, fmt.Errorf("no credentials in context")) + return + } + var data dto.WebhookRegisterBody if err := c.ShouldBindJSON(&data); err != nil { c.Status(http.StatusBadRequest) return } + usecase := api.UsecasesWithCreds(c.Request).NewWebhooksUsecase() + + err := usecase.RegisterWebhook(c.Request.Context(), + creds.OrganizationId, + null.StringFromPtr(creds.PartnerId), + models.WebhookRegister{ + EventTypes: data.EventTypes, + Url: data.Url, + HttpTimeout: data.HttpTimeout, + RateLimit: data.RateLimit, + RateLimitDuration: data.RateLimitDuration, + }) + if presentError(c, err) { + return + } + + c.Status(http.StatusOK) +} + +func (api *API) handleDeleteWebhook(c *gin.Context) { + webhookId := c.Param("webhook_id") + creds, found := utils.CredentialsFromCtx(c.Request.Context()) if !found { presentError(c, fmt.Errorf("no credentials in context")) @@ -46,19 +75,18 @@ func (api *API) handleRegisterWebhook(c *gin.Context) { usecase := api.UsecasesWithCreds(c.Request).NewWebhooksUsecase() - err := usecase.RegisterWebhook(c.Request.Context(), dto.AdaptWebhookRegister( + err := usecase.DeleteWebhook(c.Request.Context(), creds.OrganizationId, - creds.PartnerId, - data, - )) + null.StringFromPtr(creds.PartnerId), + webhookId) if presentError(c, err) { return } - c.Status(http.StatusOK) + c.Status(http.StatusNoContent) } -func (api *API) handleDeleteWebhook(c *gin.Context) { +func (api *API) handleUpdateWebhook(c *gin.Context) { webhookId := c.Param("webhook_id") creds, found := utils.CredentialsFromCtx(c.Request.Context()) @@ -67,15 +95,28 @@ func (api *API) handleDeleteWebhook(c *gin.Context) { return } + var data dto.WebhookUpdateBody + if err := c.ShouldBindJSON(&data); err != nil { + c.Status(http.StatusBadRequest) + return + } + usecase := api.UsecasesWithCreds(c.Request).NewWebhooksUsecase() - err := usecase.DeleteWebhook(c.Request.Context(), + err := usecase.UpdateWebhook(c.Request.Context(), creds.OrganizationId, null.StringFromPtr(creds.PartnerId), - webhookId) + webhookId, + models.WebhookUpdate{ + EventTypes: data.EventTypes, + Url: data.Url, + HttpTimeout: data.HttpTimeout, + RateLimit: data.RateLimit, + RateLimitDuration: data.RateLimitDuration, + }) if presentError(c, err) { return } - c.Status(http.StatusNoContent) + c.Status(http.StatusOK) } diff --git a/api/routes.go b/api/routes.go index 98d6a90b4..6d2db7c13 100644 --- a/api/routes.go +++ b/api/routes.go @@ -174,5 +174,6 @@ func (api *API) routes(auth Authentication, tokenHandler TokenHandler) { router.GET("/webhooks", api.handleListWebhooks) router.POST("/webhooks", api.handleRegisterWebhook) + router.PATCH("/webhooks/:webhook_id", api.handleUpdateWebhook) router.DELETE("/webhooks/:webhook_id", api.handleDeleteWebhook) } diff --git a/dto/webhooks.go b/dto/webhooks.go index 6a4d8a049..41b795d22 100644 --- a/dto/webhooks.go +++ b/dto/webhooks.go @@ -3,7 +3,6 @@ package dto import ( "github.com/checkmarble/marble-backend/models" "github.com/checkmarble/marble-backend/pure_utils" - "github.com/guregu/null/v5" ) type WebhookRegisterBody struct { @@ -14,21 +13,8 @@ type WebhookRegisterBody struct { RateLimitDuration *int `json:"rate_limit_duration,omitempty"` } -func AdaptWebhookRegister(organizationId string, partnerId *string, input WebhookRegisterBody) models.WebhookRegister { - return models.WebhookRegister{ - OrganizationId: organizationId, - PartnerId: null.StringFromPtr(partnerId), - Url: input.Url, - EventTypes: input.EventTypes, - HttpTimeout: input.HttpTimeout, - RateLimit: input.RateLimit, - RateLimitDuration: input.RateLimitDuration, - } -} - type Webhook struct { - EndpointId string `json:"endpoint_id"` - SubscriptionId string `json:"subscription_id"` + Id string `json:"id"` EventTypes []string `json:"event_types"` Secrets []Secret `json:"secrets"` Url string `json:"url"` @@ -59,8 +45,7 @@ func AdaptSecret(secret models.Secret) Secret { func AdaptWebhook(webhook models.Webhook) Webhook { return Webhook{ - EndpointId: webhook.EndpointId, - SubscriptionId: webhook.SubscriptionId, + Id: webhook.Id, EventTypes: webhook.EventTypes, Secrets: pure_utils.Map(webhook.Secrets, AdaptSecret), Url: webhook.Url, @@ -69,3 +54,11 @@ func AdaptWebhook(webhook models.Webhook) Webhook { RateLimitDuration: webhook.RateLimitDuration, } } + +type WebhookUpdateBody struct { + EventTypes *[]string `json:"event_types,omitempty"` + Url *string `json:"url,omitempty"` + HttpTimeout *int `json:"http_timeout,omitempty"` + RateLimit *int `json:"rate_limit,omitempty"` + RateLimitDuration *int `json:"rate_limit_duration,omitempty"` +} diff --git a/models/webhook.go b/models/webhook.go index 57cad29eb..527bbedb6 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -84,8 +84,6 @@ func (f WebhookEventFilters) MergeWithDefaults() WebhookEventFilters { } type WebhookRegister struct { - OrganizationId string - PartnerId null.String EventTypes []string Secret string Url string @@ -118,8 +116,9 @@ func NewWebhookEventCaseStatusUpdated(caseStatus CaseStatus) WebhookEventContent } type Webhook struct { - EndpointId string - SubscriptionId string + Id string + OrganizationId string + PartnerId null.String EventTypes []string Secrets []Secret Url string @@ -136,3 +135,42 @@ type Secret struct { UpdatedAt string Value string } + +type WebhookUpdate struct { + EventTypes *[]string + Url *string + HttpTimeout *int + RateLimit *int + RateLimitDuration *int +} + +// MergeWebhookWithUpdate merges a Webhook with a WebhookUpdate, returning a new Webhook with the updated fields. +// Secret is not updated by this function. +func MergeWebhookWithUpdate(w Webhook, update WebhookUpdate) Webhook { + result := Webhook{ + Id: w.Id, + OrganizationId: w.OrganizationId, + PartnerId: w.PartnerId, + EventTypes: w.EventTypes, + Url: w.Url, + HttpTimeout: w.HttpTimeout, + RateLimit: w.RateLimit, + RateLimitDuration: w.RateLimitDuration, + } + if update.EventTypes != nil { + result.EventTypes = *update.EventTypes + } + if update.Url != nil { + result.Url = *update.Url + } + if update.HttpTimeout != nil { + result.HttpTimeout = update.HttpTimeout + } + if update.RateLimit != nil { + result.RateLimit = update.RateLimit + } + if update.RateLimitDuration != nil { + result.RateLimitDuration = update.RateLimitDuration + } + return result +} diff --git a/repositories/convoy_repository.go b/repositories/convoy_repository.go index 9647ada92..775ed3a57 100644 --- a/repositories/convoy_repository.go +++ b/repositories/convoy_repository.go @@ -29,6 +29,22 @@ func getOwnerId(organizationId string, partnerId null.String) string { return fmt.Sprintf("org:%s", organizationId) } +func parseOwnerId(ownerId string) (string, null.String) { + parts := strings.Split(ownerId, "-partner:") + if len(parts) == 2 { + return parts[0][4:], null.StringFrom(parts[1]) + } + return ownerId[4:], null.String{} +} + +func getName(ownerId string, eventTypes []string) string { + eventLabel := "all-events" + if len(eventTypes) > 0 { + eventLabel = strings.Join(eventTypes, ",") + } + return fmt.Sprintf("%s|%s", ownerId, eventLabel) +} + func (repo ConvoyRepository) SendWebhookEvent(ctx context.Context, webhookEvent models.WebhookEvent) error { projectId := repo.convoyClientProvider.GetProjectID() convoyClient, err := repo.convoyClientProvider.GetClient() @@ -56,20 +72,20 @@ func (repo ConvoyRepository) SendWebhookEvent(ctx context.Context, webhookEvent return nil } -func (repo ConvoyRepository) RegisterWebhook(ctx context.Context, input models.WebhookRegister) error { +func (repo ConvoyRepository) RegisterWebhook( + ctx context.Context, + organizationId string, + partnerId null.String, + input models.WebhookRegister, +) error { projectId := repo.convoyClientProvider.GetProjectID() convoyClient, err := repo.convoyClientProvider.GetClient() if err != nil { return err } - ownerId := getOwnerId(input.OrganizationId, input.PartnerId) - - eventLabel := "all-events" - if len(input.EventTypes) > 0 { - eventLabel = strings.Join(input.EventTypes, ",") - } - name := fmt.Sprintf("%s|%s", ownerId, eventLabel) + ownerId := getOwnerId(organizationId, partnerId) + name := getName(ownerId, input.EventTypes) endpoint, err := convoyClient.CreateEndpointWithResponse(ctx, projectId, convoy.ModelsCreateEndpoint{ Name: &name, @@ -83,7 +99,7 @@ func (repo ConvoyRepository) RegisterWebhook(ctx context.Context, input models.W if err != nil { return errors.Wrap(err, "can't create convoy endpoint: request error") } - if endpoint.JSON201 != nil { + if endpoint.JSON201 == nil { err = parseResponseError(endpoint.HTTPResponse.Status, endpoint.Body) return errors.Wrap(err, "can't create convoy endpoint") } @@ -203,9 +219,12 @@ func adaptWebhook( convoyEndpoint convoy.ModelsEndpointResponse, convoySubscription convoy.ModelsSubscriptionResponse, ) models.Webhook { + organizationId, partnerId := parseOwnerId(*convoyEndpoint.OwnerId) + webhook := models.Webhook{ - SubscriptionId: *convoySubscription.Uid, - EndpointId: *convoyEndpoint.Uid, + Id: *convoyEndpoint.Uid, + OrganizationId: organizationId, + PartnerId: partnerId, EventTypes: *convoySubscription.FilterConfig.EventTypes, Url: *convoyEndpoint.Url, HttpTimeout: convoyEndpoint.HttpTimeout, @@ -222,6 +241,84 @@ func adaptWebhook( return webhook } +func getEndpoint( + ctx context.Context, + convoyClient convoy.ClientWithResponses, + projectId string, + endpointId string, +) (convoy.ModelsEndpointResponse, error) { + endpointRes, err := convoyClient.GetEndpointWithResponse(ctx, projectId, endpointId) + if err != nil { + return convoy.ModelsEndpointResponse{}, + errors.Wrap(err, "can't get convoy endpoint: request error") + } + if endpointRes.JSON200 == nil { + err = parseResponseError(endpointRes.HTTPResponse.Status, endpointRes.Body) + return convoy.ModelsEndpointResponse{}, + errors.Wrap(err, "can't get convoy endpoint") + } + + var endpoint convoy.ModelsEndpointResponse + if endpointRes.JSON200.Data != nil { + endpoint = *endpointRes.JSON200.Data + } + + return endpoint, nil +} + +func getSubscription( + ctx context.Context, + convoyClient convoy.ClientWithResponses, + projectId string, + endpointId string, +) (convoy.ModelsSubscriptionResponse, error) { + subscriptionRes, err := convoyClient.GetSubscriptionsWithResponse(ctx, projectId, &convoy.GetSubscriptionsParams{ + EndpointId: &[]string{endpointId}, + PerPage: &perPage, + }) + if err != nil { + return convoy.ModelsSubscriptionResponse{}, + errors.Wrap(err, "can't get convoy subscription: request error") + } + if subscriptionRes.JSON200 == nil { + err = parseResponseError(subscriptionRes.HTTPResponse.Status, subscriptionRes.Body) + return convoy.ModelsSubscriptionResponse{}, + errors.Wrap(err, "can't get convoy subscriptions") + } + + var subscription convoy.ModelsSubscriptionResponse + if subscriptionRes.JSON200.Data != nil && + subscriptionRes.JSON200.Data.Content != nil && + len(*subscriptionRes.JSON200.Data.Content) > 0 { + subscription = (*subscriptionRes.JSON200.Data.Content)[0] + } else { + return convoy.ModelsSubscriptionResponse{}, + errors.New("can't find convoy subscription") + } + + return subscription, nil +} + +func (repo ConvoyRepository) GetWebhook(ctx context.Context, webhookId string) (models.Webhook, error) { + projectId := repo.convoyClientProvider.GetProjectID() + convoyClient, err := repo.convoyClientProvider.GetClient() + if err != nil { + return models.Webhook{}, err + } + + endpoint, err := getEndpoint(ctx, convoyClient, projectId, webhookId) + if err != nil { + return models.Webhook{}, err + } + + subscription, err := getSubscription(ctx, convoyClient, projectId, webhookId) + if err != nil { + return models.Webhook{}, err + } + + return adaptWebhook(endpoint, subscription), nil +} + func (repo ConvoyRepository) DeleteWebhook(ctx context.Context, webhookId string) error { projectId := repo.convoyClientProvider.GetProjectID() convoyClient, err := repo.convoyClientProvider.GetClient() @@ -242,6 +339,59 @@ func (repo ConvoyRepository) DeleteWebhook(ctx context.Context, webhookId string return nil } +func (repo ConvoyRepository) UpdateWebhook( + ctx context.Context, + input models.Webhook, +) error { + projectId := repo.convoyClientProvider.GetProjectID() + convoyClient, err := repo.convoyClientProvider.GetClient() + if err != nil { + return err + } + + ownerId := getOwnerId(input.OrganizationId, input.PartnerId) + name := getName(ownerId, input.EventTypes) + + subscription, err := getSubscription(ctx, convoyClient, projectId, input.Id) + if err != nil { + return err + } + + endpointRes, err := convoyClient.UpdateEndpointWithResponse(ctx, projectId, input.Id, convoy.ModelsUpdateEndpoint{ + Name: &name, + OwnerId: &ownerId, + Url: &input.Url, + HttpTimeout: input.HttpTimeout, + RateLimit: input.RateLimit, + RateLimitDuration: input.RateLimitDuration, + }) + if err != nil { + return errors.Wrap(err, "can't update convoy endpoint: request error") + } + if endpointRes.JSON202 == nil { + err = parseResponseError(endpointRes.HTTPResponse.Status, endpointRes.Body) + return errors.Wrap(err, "can't update convoy endpoint") + } + + subscriptionRes, err := convoyClient.UpdateSubscriptionWithResponse(ctx, + projectId, + *subscription.Uid, + convoy.ModelsUpdateSubscription{ + FilterConfig: &convoy.ModelsFilterConfiguration{ + EventTypes: &input.EventTypes, + }, + }) + if err != nil { + return errors.Wrap(err, "can't update convoy subscription: request error") + } + if subscriptionRes.JSON202 == nil { + err = parseResponseError(subscriptionRes.HTTPResponse.Status, subscriptionRes.Body) + return errors.Wrap(err, "can't update convoy subscription") + } + + return nil +} + func parseResponseError(status string, body []byte) error { var dest struct { Message *string `json:"message,omitempty"` diff --git a/usecases/security/enforce_security_webhook.go b/usecases/security/enforce_security_webhook.go index f2ca88480..243d622ba 100644 --- a/usecases/security/enforce_security_webhook.go +++ b/usecases/security/enforce_security_webhook.go @@ -11,23 +11,29 @@ import ( ) func (e *EnforceSecurityImpl) SendWebhookEvent(ctx context.Context, organizationId string, partnerId null.String) error { - err := errors.Join( + return errors.Join( e.Permission(models.WEBHOOK_EVENT), - utils.EnforceOrganizationAccess(e.Credentials, organizationId), + utils.EnforceOrganizationAndPartnerAccess(e.Credentials, organizationId, partnerId), ) - if partnerId.Valid { - err = errors.Join(err, utils.EnforcePartnerAccess(e.Credentials, partnerId.String)) - } - return err } -func (e *EnforceSecurityImpl) CanManageWebhook(ctx context.Context, organizationId string, partnerId null.String) error { - err := errors.Join( +func (e *EnforceSecurityImpl) CanCreateWebhook(ctx context.Context, organizationId string, partnerId null.String) error { + return errors.Join( e.Permission(models.WEBHOOK), - utils.EnforceOrganizationAccess(e.Credentials, organizationId), + utils.EnforceOrganizationAndPartnerAccess(e.Credentials, organizationId, partnerId), + ) +} + +func (e *EnforceSecurityImpl) CanReadWebhook(ctx context.Context, webhook models.Webhook) error { + return errors.Join( + e.Permission(models.WEBHOOK), + utils.EnforceOrganizationAndPartnerAccess(e.Credentials, webhook.OrganizationId, webhook.PartnerId), + ) +} + +func (e *EnforceSecurityImpl) CanModifyWebhook(ctx context.Context, webhook models.Webhook) error { + return errors.Join( + e.Permission(models.WEBHOOK), + utils.EnforceOrganizationAndPartnerAccess(e.Credentials, webhook.OrganizationId, webhook.PartnerId), ) - if partnerId.Valid { - err = errors.Join(err, utils.EnforcePartnerAccess(e.Credentials, partnerId.String)) - } - return err } diff --git a/usecases/webhooks_usecase.go b/usecases/webhooks_usecase.go index c112dd471..edc52fb59 100644 --- a/usecases/webhooks_usecase.go +++ b/usecases/webhooks_usecase.go @@ -13,13 +13,17 @@ import ( ) type convoyWebhooksRepository interface { + GetWebhook(ctx context.Context, webhookId string) (models.Webhook, error) ListWebhooks(ctx context.Context, organizationId string, partnerId null.String) ([]models.Webhook, error) - RegisterWebhook(ctx context.Context, input models.WebhookRegister) error + RegisterWebhook(ctx context.Context, organizationId string, partnerId null.String, input models.WebhookRegister) error + UpdateWebhook(ctx context.Context, input models.Webhook) error DeleteWebhook(ctx context.Context, webhookId string) error } type enforceSecurityWebhook interface { - CanManageWebhook(ctx context.Context, organizationId string, partnerId null.String) error + CanCreateWebhook(ctx context.Context, organizationId string, partnerId null.String) error + CanReadWebhook(ctx context.Context, webhook models.Webhook) error + CanModifyWebhook(ctx context.Context, webhook models.Webhook) error } type WebhooksUsecase struct { @@ -44,24 +48,27 @@ func NewWebhooksUsecase( } func (usecase WebhooksUsecase) ListWebhooks(ctx context.Context, organizationId string, partnerId null.String) ([]models.Webhook, error) { - err := usecase.enforceSecurity.CanManageWebhook(ctx, organizationId, partnerId) - if err != nil { - return nil, err - } - webhooks, err := usecase.convoyRepository.ListWebhooks(ctx, organizationId, partnerId) if err != nil { return nil, errors.Wrap(err, "error listing webhooks") } + for _, webhook := range webhooks { + if err := usecase.enforceSecurity.CanReadWebhook(ctx, webhook); err != nil { + return nil, err + } + } + return webhooks, nil } func (usecase WebhooksUsecase) RegisterWebhook( ctx context.Context, + organizationId string, + partnerId null.String, input models.WebhookRegister, ) error { - err := usecase.enforceSecurity.CanManageWebhook(ctx, input.OrganizationId, input.PartnerId) + err := usecase.enforceSecurity.CanCreateWebhook(ctx, organizationId, partnerId) if err != nil { return err } @@ -72,7 +79,7 @@ func (usecase WebhooksUsecase) RegisterWebhook( input.Secret = generateSecret() - err = usecase.convoyRepository.RegisterWebhook(ctx, input) + err = usecase.convoyRepository.RegisterWebhook(ctx, organizationId, partnerId, input) if err != nil { return errors.Wrap(err, "error registering webhook") } @@ -92,10 +99,28 @@ func generateSecret() string { func (usecase WebhooksUsecase) DeleteWebhook( ctx context.Context, organizationId string, partnerId null.String, webhookId string, ) error { - err := usecase.enforceSecurity.CanManageWebhook(ctx, organizationId, partnerId) + webhook, err := usecase.convoyRepository.GetWebhook(ctx, webhookId) + if err != nil { + return models.NotFoundError + } + if err = usecase.enforceSecurity.CanModifyWebhook(ctx, webhook); err != nil { + return err + } + + return usecase.convoyRepository.DeleteWebhook(ctx, webhook.Id) +} + +func (usecase WebhooksUsecase) UpdateWebhook( + ctx context.Context, organizationId string, partnerId null.String, webhookId string, input models.WebhookUpdate, +) error { + webhook, err := usecase.convoyRepository.GetWebhook(ctx, webhookId) if err != nil { + return models.NotFoundError + } + if err = usecase.enforceSecurity.CanModifyWebhook(ctx, webhook); err != nil { return err } - return usecase.convoyRepository.DeleteWebhook(ctx, webhookId) + return usecase.convoyRepository.UpdateWebhook(ctx, + models.MergeWebhookWithUpdate(webhook, input)) } diff --git a/utils/organization_security.go b/utils/organization_security.go index 3a1262ecb..62055119e 100644 --- a/utils/organization_security.go +++ b/utils/organization_security.go @@ -2,6 +2,7 @@ package utils import ( "github.com/checkmarble/marble-backend/models" + "github.com/guregu/null/v5" "github.com/cockroachdb/errors" ) @@ -39,3 +40,11 @@ func EnforcePartnerAccess(creds models.Credentials, partnerId string) error { } return nil } + +func EnforceOrganizationAndPartnerAccess(creds models.Credentials, organizationId string, partnerId null.String) error { + err := EnforceOrganizationAccess(creds, organizationId) + if partnerId.Valid { + err = errors.Join(err, EnforcePartnerAccess(creds, partnerId.String)) + } + return err +}